---
title: 'Check your return values: APIs'
teaser: 'Learn how to apply a little forethought and discipline to provide your users
  a more informative and stable app experience.

  '
tags: web,rails
author: Mike Burns
published_on: 2020-08-17
---

This is the API-focused sequel to [the Web-focused post]. As before, the main
take-away from this post is to **always check return values**. A corollary is
to **design the user experience to include error messages in a helpful
manner**.

[the Web-focused post]: https://thoughtbot.com/blog/check-return-values-web

## APIs have errors too

The second example is the more complex flow: sync and async with a service
object behind an API. We need to report as many errors as we can -- if we're
performing a synchronous task, we need to dispatch on its result -- but also
save space for errors further down the road. The UI dictates various pieces of
communication, and we need to fulfill that communication.

<p><img
src="https://images.thoughtbot.com/handle-failure/FGgO5ZiaRxeaxdq2Ki0u_68747470733a2f2f696d616765732e74686f75676874626f742e636f6d2f68616e646c652d6661696c7572652f64574871747678655255796d49564d79506975445f636865636b2d6f75742e706e67-1x.png"
srcset="https://images.thoughtbot.com/handle-failure/dWHqtvxeRUymIVMyPiuD_check-out.png 2x"
alt="credit card details screen"></p>

As mentioned before this is a two-way street: the user experience team that
builds the UI needs to work with the developers to understand what can go
wrong. Sometimes the developers won't find out until implementation is
underway, and so the designers need to stick around and be prepared for an
overhaul.

<p><img
src="https://images.thoughtbot.com/handle-failure/QZRYlAoiTOmzuIVS7Y5p_68747470733a2f2f696d616765732e74686f75676874626f742e636f6d2f68616e646c652d6661696c7572652f3251346f46724d34524b574c7555367148354b445f636865636b2d6f75742d6572726f722d67656e657269632e706e67-1x.png"
srcset="https://images.thoughtbot.com/handle-failure/2Q4oFrM4RKWLuU6qH5KD_check-out-error-generic.png 2x"
alt="payment screen with a generic error message"></p>

With an API there is a level of indirection: the UI team, mobile and other
client teams, and backend team must work together to understand all that can go
wrong and how the user should know about it.

<p><img
src="https://images.thoughtbot.com/handle-failure/FIKqT12QF5mGcxETQHA9_68747470733a2f2f696d616765732e74686f75676874626f742e636f6d2f68616e646c652d6661696c7572652f46644876586f55655251366334594b36775931695f636865636b2d6f75742d6572726f722d65787069726174696f6e2e706e67-1x.png"
srcset="https://images.thoughtbot.com/handle-failure/FdHvXoUeRQ6c4YK6wY1i_check-out-error-expiration.png 2x"
alt="payment screen with an invalid expiration date"></p>

Given what we know, we need to save the current checkout details, kick off a
background job, and report what we can. We'll use a [service object] to wrap
some of these complexities; `Checkout#commit` saves the state and kicks off a
background job:

[service object]: https://avdi.codes/service-objects/

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Checkout</span> <span class="o">&lt;</span> <span class="no">Mutations</span><span class="o">::</span><span class="no">BaseMutation</span>&#x000A;  <span class="n">argument</span> <span class="ss">:input</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">CheckoutInput</span><span class="p">,</span> <span class="ss">required: </span><span class="kp">true</span>&#x000A;  <span class="n">type</span> <span class="no">Types</span><span class="o">::</span><span class="no">CheckoutOutput</span>&#x000A;&#x000A;  <span class="k">def</span> <span class="nf">resolve</span><span class="p">(</span><span class="n">input</span><span class="p">:)</span>&#x000A;    <span class="n">checkout</span> <span class="o">=</span> <span class="no">Checkout</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="ss">user: </span><span class="n">current_user</span><span class="p">))</span>&#x000A;&#x000A;    <span class="k">if</span> <span class="n">job</span> <span class="o">=</span> <span class="n">checkout</span><span class="p">.</span><span class="nf">commit</span>&#x000A;      <span class="p">{</span> <span class="ss">checkout: </span><span class="n">checkout</span><span class="p">,</span> <span class="ss">job: </span><span class="n">job</span><span class="p">,</span> <span class="ss">errors: </span><span class="p">[],</span> <span class="ss">status: :in_progress</span> <span class="p">}</span>&#x000A;    <span class="k">else</span>&#x000A;      <span class="p">{</span> <span class="ss">checkout: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">job: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">errors: </span><span class="n">checkout</span><span class="p">.</span><span class="nf">errors</span><span class="p">,</span> <span class="ss">status: :error</span> <span class="p">}</span>&#x000A;    <span class="k">end</span>&#x000A;  <span class="k">end</span>&#x000A;&#x000A;  <span class="kp">private</span>&#x000A;&#x000A;  <span class="k">def</span> <span class="nf">current_user</span>&#x000A;    <span class="n">context</span><span class="p">[</span><span class="ss">:current_user</span><span class="p">]</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>This has some boilerplate for the GraphQL library, but the big
picture is similar to a controller. Create an instance of the
<code>Checkout</code> service object and call the method on it
(<code>#commit</code>). This returns an object for tracking the background job
on success; on failure it returns <code>nil</code> and sets errors on the
<code>checkout</code> instance. Finally we return either the checkout and job
objects with an in-progress status, or the errors with an error
status.</figcaption>
</figure>

This represents the "just before the view" point in an API. Let's see what it
takes to fill in the rest.

## Playing with the full orchestra

Let's start the service object by scoping out a naive implementation of our
`Checkout#commit` method. This has a bug!

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Checkout</span>&#x000A;  <span class="kp">extend</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Naming</span>&#x000A;&#x000A;  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>&#x000A;    <span class="vi">@user</span> <span class="o">=</span> <span class="n">args</span><span class="p">[</span><span class="ss">:user</span><span class="p">]</span>&#x000A;    <span class="vi">@payment_token</span> <span class="o">=</span> <span class="n">args</span><span class="p">[</span><span class="ss">:payment_token</span><span class="p">]</span>&#x000A;    <span class="vi">@order</span> <span class="o">=</span> <span class="n">args</span><span class="p">[</span><span class="ss">:order</span><span class="p">]</span>&#x000A;    <span class="vi">@errors</span> <span class="o">=</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Errors</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>&#x000A;  <span class="k">end</span>&#x000A;&#x000A;  <span class="c1">##</span>&#x000A;  <span class="c1"># @return [ActiveModel::Errors] checkout errors</span>&#x000A;  <span class="nb">attr_reader</span> <span class="ss">:errors</span>&#x000A;&#x000A;  <span class="c1">##</span>&#x000A;  <span class="c1"># Advance the cart to the next step, preventing the user from double-checking</span>&#x000A;  <span class="c1"># out. Enqueue a job to complete the charge.</span>&#x000A;  <span class="c1">#</span>&#x000A;  <span class="c1"># On failure, sets errors on {#errors}.</span>&#x000A;  <span class="c1">#</span>&#x000A;  <span class="c1"># @return the [ActiveJob] instance, or falsy on failure</span>&#x000A;  <span class="k">def</span> <span class="nf">commit</span>&#x000A;    <span class="vi">@order</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">state: :processing</span><span class="p">)</span>&#x000A;    <span class="no">PaymentProcessorJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="vi">@payment_token</span><span class="p">,</span> <span class="vi">@order</span><span class="p">)</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>Our <code>Checkout</code> service obect takes the arguments from
the API: user, payment token, and the order we are purchasing. We make use of
Rails' error objects via the <code>ActiveModel::Naming</code> module and the
<code>ActiveModel::Errors</code> class.

The actual checkout action (<code>#commit</code>) does two things: update the
order's state, and enqueue a background job to communicate with the payment
processor. This method has a bug.</figcaption>
</figure>

The issue is that we've violated our contract: we need `#errors` to produce any
errors that we can generate synchronously. Our call to `#update` could generate
useful errors -- for example, if the user tries to press "Checkout" in a stale
tab on an old cart -- but we drop them into the bit bucket instead.  Moreover,
we march forward on an invalid update, enqueuing the job to charge the customer
regardless.

By now we're deep down the code path, far from the ERb view rendering this.
Whatever decisions we made up the callstack are going to be hard to change now.
This is often where people reach for exceptions. Note that a stale cart is not
an exceptional situation; we're reaching for an exception instead because we
coded ourselves into a corner.

```ruby
def commit
  @order.update!(state: :processing) # changed from a non-raising #update
  PaymentProcessorJob.perform_later(@payment_token, @order)
end
```

And honestly, I'd rather see `#update!` than an unchecked `#update`. At least
it's not hiding a bug.

But the better solution -- and, luckily, the actual design we had set ourselves
up for -- is to leave the exceptions for the exceptional cases but to treat
a failed `#update` as something we can tell the user about.

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">commit</span>&#x000A;  <span class="k">if</span> <span class="vi">@order</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">state: :processing</span><span class="p">)</span>&#x000A;    <span class="no">PaymentProcessorJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="vi">@payment_token</span><span class="p">,</span> <span class="vi">@order</span><span class="p">)</span>&#x000A;  <span class="k">else</span>&#x000A;    <span class="vi">@errors</span><span class="p">.</span><span class="nf">merge!</span><span class="p">(</span><span class="vi">@order</span><span class="p">.</span><span class="nf">errors</span><span class="p">)</span>&#x000A;    <span class="kp">false</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>Here we make use of the fact that <code>#update</code> returns
whether it succeeded. On success, proceed as before: enqueue a payment job. On
failure set the errors on ourselves and return false. This fulfills the
contract of the method.</figcaption>
</figure>

Let's go into detail about our method calls.

As was discussed with `#find_by`, an `#update` can raise an exception on
database failures. These are programmer bugs, which we are letting bubble up.
It's good that we're considering these exceptions and recognizing how we want
to handle them.

Next up is the [`#perform_later`] method, part of `ActiveJob`. This is
documented to return an instance of the job class; it does this by [calling]
the [`#enqueue`] method, which returns the job instance, or `false` on failure.
This is exactly what we want out of our `#commit` method, so we can just return
that. Perfect.

[`#perform_later`]: https://api.rubyonrails.org/classes/ActiveJob/Enqueuing/ClassMethods.html#method-i-perform_later
[calling]: https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/activejob/lib/active_job/enqueuing.rb#L22
[`#enqueue`]: https://api.rubyonrails.org/classes/ActiveJob/Enqueuing.html#method-i-enqueue

Perfect? What if the order is marked processing, but the payment processor
fails to enqueue? What if the underlying queue adapter raises (such as during a
lost Redis connection)?

Let's try to orchestrate these two error-producing things:

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">commit</span>&#x000A;  <span class="k">if</span> <span class="vi">@order</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">state: :processing</span><span class="p">)</span>&#x000A;    <span class="n">enqueue_payment_job</span>&#x000A;  <span class="k">else</span>&#x000A;    <span class="vi">@errors</span><span class="p">.</span><span class="nf">merge!</span><span class="p">(</span><span class="vi">@order</span><span class="p">.</span><span class="nf">errors</span><span class="p">)</span>&#x000A;    <span class="kp">false</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;&#x000A;<span class="kp">private</span>&#x000A;&#x000A;<span class="k">def</span> <span class="nf">enqueue_payment_job</span>&#x000A;  <span class="n">job</span> <span class="o">=</span> <span class="no">PaymentProcessorJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="vi">@payment_token</span><span class="p">,</span> <span class="vi">@order</span><span class="p">)</span>&#x000A;&#x000A;  <span class="k">return</span> <span class="n">job</span> <span class="k">if</span> <span class="n">job</span>&#x000A;&#x000A;  <span class="vi">@errors</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="ss">:base</span><span class="p">,</span> <span class="ss">:job_failed</span><span class="p">)</span>&#x000A;&#x000A;  <span class="k">unless</span> <span class="vi">@order</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">state: :pending</span><span class="p">)</span>&#x000A;    <span class="vi">@errors</span><span class="p">.</span><span class="nf">merge!</span><span class="p">(</span><span class="vi">@order</span><span class="p">.</span><span class="nf">errors</span><span class="p">)</span>&#x000A;  <span class="k">end</span>&#x000A;&#x000A;  <span class="kp">false</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>This is our first attempt at handling issues with our job runner.
Instead of merely enqueing a job and passing the result off as if it worked, we
check it manually. If it worked, return the job. Otherwise we add a message to
our errors object, update the order's state back to pending, handle failures
from that update, and return false. It's a large amount of additional
complexity.</figcaption>
</figure>

Ick. How about we enqueue it first, with a delay, then cancel it ASAP if the
`#update` fails?

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">commit</span>&#x000A;  <span class="k">if</span> <span class="n">job</span> <span class="o">=</span> <span class="no">PaymentProcessorJob</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@payment_token</span><span class="p">,</span> <span class="vi">@order</span><span class="p">).</span><span class="nf">enqueue</span><span class="p">(</span><span class="ss">wait: </span><span class="mi">5</span><span class="p">)</span>&#x000A;    <span class="k">if</span> <span class="vi">@order</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">state: :processing</span><span class="p">)</span>&#x000A;      <span class="n">job</span>&#x000A;    <span class="k">else</span>&#x000A;      <span class="n">job</span><span class="p">.</span><span class="nf">cancel</span>&#x000A;      <span class="vi">@errors</span><span class="p">.</span><span class="nf">merge!</span><span class="p">(</span><span class="vi">@order</span><span class="p">.</span><span class="nf">errors</span><span class="p">)</span>&#x000A;      <span class="kp">false</span>&#x000A;    <span class="k">end</span>&#x000A;  <span class="k">else</span>&#x000A;    <span class="vi">@errors</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="ss">:base</span><span class="p">,</span> <span class="ss">:job_failed</span><span class="p">)</span>&#x000A;    <span class="kp">false</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>In this second attempt at handling issues from the job runner, we
first enqueue and then set the order's state. We delay the job by five seconds,
which gives us ample time to finish executing the rest of this method. If the
order state update succeeds, we return the job as before. If it fails, we
cancel the job, set our errors, and return false. If the job itself fails we
add a message to our errors and return false. This is all a bit lengthy and
nested, but it is step-by-step.</figcaption>
</figure>

No one said this was easy.

## I'd like to subscribe to your error newsletter

This covers the synchronous errors, but now we have a background job that is
about to make a network request. As [the comic] says, "oh no!"

[the comic]: https://webcomicname.com/ "webcomic name"

<p><img
src="https://images.thoughtbot.com/handle-failure/CylKpt5QWevve0NSAD4s_68747470733a2f2f696d616765732e74686f75676874626f742e636f6d2f68616e646c652d6661696c7572652f70596c7575457a525138616b54775a484c3941495f70657273697374656e742d6572726f722e706e67-1x.png"
srcset="https://images.thoughtbot.com/handle-failure/pYluuEzRQ8akTwZHL9AI_persistent-error.png 2x"
alt="an alert about a failed background job"></p>

We need to communicate these errors back via the API. Assuming GraphQL and a
patient mobile development team, we're talking about a [subscription]. We'll
listen for subscription requests by the job ID, and on job updates we'll send
back a job status.

[subscription]: http://spec.graphql.org/June2018/#sec-Subscription

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">JobStatusChanged</span> <span class="o">&lt;</span> <span class="no">Subscriptions</span><span class="o">::</span><span class="no">BaseSubscription</span>&#x000A;  <span class="n">field</span> <span class="ss">:provider_job_id</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">Integer</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>&#x000A;  <span class="n">type</span> <span class="no">Types</span><span class="o">::</span><span class="no">JobStatus</span>&#x000A;&#x000A;  <span class="k">def</span> <span class="nf">update</span><span class="p">(</span><span class="n">provider_job_id</span><span class="p">:)</span>&#x000A;    <span class="n">job_status</span> <span class="o">=</span> <span class="n">object</span>&#x000A;&#x000A;    <span class="k">if</span> <span class="n">job_status</span><span class="p">.</span><span class="nf">success?</span> <span class="o">||</span> <span class="n">job_status</span><span class="p">.</span><span class="nf">failure?</span>&#x000A;      <span class="n">unsubscribe</span>&#x000A;    <span class="k">end</span>&#x000A;&#x000A;    <span class="k">super</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>This is a GraphQL subscription, making it another "just before the
view" class.  The client will request this subscription with the background job
ID (known as the <code>provider_job_id</code> in ActiveJob). When our job would
like to broadcast an update to its subscribers, it will communicate this to
graphql-ruby via a custom JobStatus object. If the status is a success or
failure -- if it is finished -- we unsubscribe the client; we will have nothing
more to say.  Regardless we broadcast out the job status for the client (via
<code>super</code>).</figcaption>
</figure>

We need more infrastructure. Here's the first pass of the background job:

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">PaymentProcessorJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>&#x000A;  <span class="c1">##</span>&#x000A;  <span class="c1"># Capture the payment then notify the subscriptions about how it went.</span>&#x000A;  <span class="c1">#</span>&#x000A;  <span class="c1"># @return void</span>&#x000A;  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">payment_token</span><span class="p">,</span> <span class="n">order</span><span class="p">)</span>&#x000A;    <span class="no">PaymentProvider</span><span class="p">.</span><span class="nf">capture!</span><span class="p">(</span><span class="n">payment_token</span><span class="p">,</span> <span class="n">order</span><span class="p">.</span><span class="nf">total_price</span><span class="p">)</span>&#x000A;    <span class="n">order</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">state: :completed</span><span class="p">)</span>&#x000A;&#x000A;    <span class="n">notify_graphql</span><span class="p">(</span><span class="ss">:ok</span><span class="p">)</span>&#x000A;  <span class="k">rescue</span> <span class="no">PaymentProvider</span><span class="o">::</span><span class="no">Error</span> <span class="o">=&gt;</span> <span class="n">e</span>&#x000A;    <span class="n">notify_graphql</span><span class="p">(</span><span class="ss">:fail</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>&#x000A;  <span class="k">end</span>&#x000A;&#x000A;  <span class="kp">private</span>&#x000A;&#x000A;  <span class="k">def</span> <span class="nf">notify_graphql</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">exn</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>&#x000A;    <span class="no">OurAppGraphQLSchema</span><span class="p">.</span><span class="nf">subscriptions</span><span class="p">.</span><span class="nf">trigger</span><span class="p">(</span>&#x000A;      <span class="s2">"jobStatusChanged"</span><span class="p">,</span>&#x000A;      <span class="p">{</span> <span class="ss">provider_job_id: </span><span class="n">provider_job_id</span> <span class="p">},</span>&#x000A;      <span class="n">job_status_for</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">exn</span><span class="p">),</span>&#x000A;    <span class="p">)</span>&#x000A;  <span class="k">end</span>&#x000A;&#x000A;  <span class="k">def</span> <span class="nf">job_status_for</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">exn</span><span class="p">)</span>&#x000A;    <span class="k">case</span> <span class="n">status</span>&#x000A;    <span class="k">when</span> <span class="ss">:ok</span>&#x000A;      <span class="no">JobStatus</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">success: </span><span class="kp">true</span><span class="p">)</span>&#x000A;    <span class="k">when</span> <span class="ss">:fail</span>&#x000A;      <span class="no">JobStatus</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">success: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">errors: </span><span class="no">Array</span><span class="p">(</span><span class="n">exn</span><span class="p">.</span><span class="nf">message</span><span class="p">))</span>&#x000A;    <span class="k">end</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>There's a bit here in our first attempt at a background job. In the
main method we call our underlying payment provider to capture the payment
capture, then we mark the order as completed. For success or error, we notify
graphql-ruby to notify all subscribers listening for details for this job. This
is communicated via a simple <code>JobStatus</code> data class we
create.</figcaption>
</figure>

Our hypothetical payment provider offers a `#capture!` method to finalize the
charge, but it raises an exception. So here we have to decide: do we let this
exception bubble, or do we transform it into something else?

Our goal is to notify our API subscribers. We programmed ourselves into a
corner again: an exception would float into the ether, logged into our
exception tracker, disappearing from the user's view. We don't want that. So we
handle the exception that we expect: rescue it and notify our API subscribers.

Astute readers will notice a mistake we've mentioned already, but there are two
other errors that stand out to me.

We are using ActiveJob's [GlobalID] mechanism to pass full objects through to
the job. If the `Order` object is deleted from the database before this job
runs (remember that five second delay at the end of the prior section?), this
will [cause ActiveJob to raise an `ActiveJob::DeserializationError`]. That
said, our `Order` objects are permanent; if one is deleted, this is a
programmer error, and should bubble up to our exception tracker.

[GlobalID]: https://guides.rubyonrails.org/active_job_basics.html#globalid
[cause ActiveJob to raise an `ActiveJob::DeserializationError`]: https://github.com/rails/rails/blob/94d2aef7971d7a1c7e1409dcf1a48cf7b8c2ea0d/activejob/lib/active_job/arguments.rb#L44

However however!, we would violate our contract -- to notify our API
subscribers -- if we simply bubbled up; we need to notify along the way. Let's
use `rescue_from` to handle all exceptions with a notification and then
re-raising:

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">PaymentProcessorJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>&#x000A;  <span class="n">rescue_from</span> <span class="no">StandardError</span> <span class="k">do</span> <span class="o">|</span><span class="n">job</span><span class="p">,</span> <span class="n">exn</span><span class="o">|</span>&#x000A;    <span class="n">job</span><span class="p">.</span><span class="nf">notify_graphql</span><span class="p">(</span><span class="ss">:fail</span><span class="p">,</span> <span class="n">exn</span><span class="p">)</span>&#x000A;    <span class="k">raise</span> <span class="n">exn</span>&#x000A;  <span class="k">end</span>&#x000A;&#x000A;  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">payment_token</span><span class="p">,</span> <span class="n">order</span><span class="p">)</span>&#x000A;    <span class="no">PaymentProvider</span><span class="p">.</span><span class="nf">capture!</span><span class="p">(</span><span class="n">payment_token</span><span class="p">,</span> <span class="n">order</span><span class="p">.</span><span class="nf">total_price</span><span class="p">)</span>&#x000A;    <span class="n">order</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">state: :completed</span><span class="p">)</span>&#x000A;&#x000A;    <span class="n">notify_graphql</span><span class="p">(</span><span class="ss">:ok</span><span class="p">)</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>Use the ActiveJob <code>rescue_from</code> control method to notify
all subscribers about all exceptions, and then re-raise the
exception.</figcaption>
</figure>

The next issue that stands out to me is the fact that our payment provider is
making a network call. This can lead to [timeouts], DNS errors, TLS errors,
[TCP] errors, and HTTP protocol errors. Those are most definitely going to come
in as [exceptions]. Some of these exceptions mean we should try again in a
minute, and some don't.

[timeouts]: https://robertovitillo.com/default-timeouts/
[TCP]: https://robertovitillo.com/what-every-developer-should-know-about-tcp/
[exceptions]: https://github.com/thoughtbot/suspenders/blob/master/templates/errors.rb#L11

The retry is a notable aspect often left out of UI-related async discussions.
The user will have some expectation that things are going well unless they hear
of an error. After a discussion with the product team, everyone agrees to let
the API know when a job is taking longer than expected, to allow the mobile
team to experiment with informing the user.

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">PaymentProcessorJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>&#x000A;  <span class="n">discard_on</span> <span class="no">EOFError</span><span class="p">,</span> <span class="no">Errno</span><span class="o">::</span><span class="no">ECONNRESET</span><span class="p">,</span> <span class="no">Errno</span><span class="o">::</span><span class="no">EINVAL</span><span class="p">,</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTPBadResponse</span><span class="p">,</span>&#x000A;    <span class="no">Timeout</span><span class="o">::</span><span class="no">Error</span><span class="p">,</span> <span class="no">ActiveJob</span><span class="o">::</span><span class="no">DeserializationError</span>&#x000A;&#x000A;  <span class="n">retry_on</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTPHeaderSyntaxError</span><span class="p">,</span> <span class="no">Net</span><span class="o">::</span><span class="no">ProtocolError</span> <span class="k">do</span> <span class="o">|</span><span class="n">job</span><span class="p">,</span> <span class="n">exn</span><span class="o">|</span>&#x000A;    <span class="n">job</span><span class="p">.</span><span class="nf">notify_graphql</span><span class="p">(</span><span class="ss">:retry</span><span class="p">,</span> <span class="n">exn</span><span class="p">)</span>&#x000A;  <span class="k">end</span>&#x000A;&#x000A;  <span class="kp">private</span>&#x000A;&#x000A;  <span class="k">def</span> <span class="nf">job_status_for</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="n">exn</span><span class="p">)</span>&#x000A;    <span class="k">case</span> <span class="n">status</span>&#x000A;    <span class="k">when</span> <span class="ss">:ok</span>&#x000A;      <span class="no">JobStatus</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">success: </span><span class="kp">true</span><span class="p">)</span>&#x000A;    <span class="k">when</span> <span class="ss">:fail</span>&#x000A;      <span class="no">JobStatus</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">success: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">errors: </span><span class="no">Array</span><span class="p">(</span><span class="n">exn</span><span class="p">.</span><span class="nf">message</span><span class="p">))</span>&#x000A;    <span class="k">when</span> <span class="ss">:retry</span>&#x000A;      <span class="no">JobStatus</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">success: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">status: :retry</span><span class="p">)</span>&#x000A;    <span class="k">end</span>&#x000A;  <span class="k">end</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>Here we make use of ActiveJob's <code>discard_on</code> and
<code>retry_on</code> control methods to describe what to do on various
exceptions. If we retry then we notify the subscribers with that fact, through
our <code>JobStatus</code> data object.</figcaption>
</figure>

With that in place, this example still fails to apply lessons from an earlier
section: we call `#update` without checking the return value. But what does it
mean for the `#update` to fail? Again, a corner case that did not come up
during the design.

For the `#update` to fail would mean that the customer is charged but the order
is in a processing state. It will appear in their list of in-progress purchases
-- at this point, forever -- but their credit card bill will reflect the
reality that they have paid for the product. And without moving to the
completed state, the fulfillment center will never see it.

The stakes are high, but luckily this specific failure is rare. The solution
here can be as simple as notifying the customer support team but otherwise
treating this as a success.

<figure>

<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">payment_token</span><span class="p">,</span> <span class="n">order</span><span class="p">)</span>&#x000A;  <span class="no">PaymentProvider</span><span class="p">.</span><span class="nf">capture!</span><span class="p">(</span><span class="n">payment_token</span><span class="p">,</span> <span class="n">order</span><span class="p">.</span><span class="nf">total_price</span><span class="p">)</span>&#x000A;&#x000A;  <span class="k">unless</span> <span class="n">order</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">state: :completed</span><span class="p">)</span>&#x000A;    <span class="no">Mailer</span><span class="p">.</span><span class="nf">notify_cx_about_stuck_order</span><span class="p">(</span><span class="n">payment_token</span><span class="p">,</span> <span class="n">order</span><span class="p">)</span>&#x000A;  <span class="k">end</span>&#x000A;&#x000A;  <span class="n">notify_graphql</span><span class="p">(</span><span class="ss">:ok</span><span class="p">)</span>&#x000A;<span class="k">end</span>&#x000A;</code></pre></div>

<figcaption>We've updated the main method of the background job to check the
return value from <code>#update</code>. If it fails, we notify our customer
support team and move on with life.</figcaption>
</figure>

Let's leave the mail failure for the exception tracker. This allows us to
complete the checkout flow just in time to end this article.

## Conclusion

We followed two common examples -- authentication and checkout -- through all
their error handling woes. Along the way we uncovered corner cases and found
places where the design did or did not lend itself to helping the user through
failure. The design dictated the implementation; thinking about errors up-front
helped us build out a system for handling most of what can be thrown at us.

For each method we used, we thought through its failure modes. This included
its return value, its documented inputs, and its exceptions both known and
undocumented. We bucketed failures into user-fixable, user-visible, and
programmer error, and we handled each bucket differently.

Following this practice exposed a stable series of patterns that will reduce
the growing noise of exceptions accumulating in the exception tracker and
also reduce the number of bugs hiding for users to discover after the next
refactoring.

With a little forethought and discipline, our users can have a more informative
and stable app experience.

(Thank you to [Eric Bailey] for his wonderful UI mocks used throughout this
post.)

[Eric Bailey]: https://thoughtbot.com/blog/authors/eric-bailey
