Error Handling Styles For API Servers

This is a blog about the development of Yeller, The Exception Tracker with Answers

Read more about Yeller here

A few weeks back, some folk I talk to discussed error handling styles. As part of the discussion, I dropped the toplevel code for Yeller’s api handler (the part of the system that receives exception data and puts it into a queue).

(defn receive-api-call [id-generator tokens-bucket enqueue request]
  (try
    (err->>
      request
      reject-invalid
      parse-request
      (validate-tokens tokens-bucket)
      sanitize/sanitize-exception
      (embellish-exception id-generator)
      serialize
      enqueue)
    (catch Exception e
      (.printStackTrace e)
      (mark! respond-unknown-exception)
      (riemann/send-exception e)
      {:status 500
       :body "Failed to track exception, our api hit an error. We've been notified and are working on it"})))

This is by far my favorite error handling style for for API servers:

Compose your functions that can error with a thing that stops the chain as soon as one fails

Specifically the important part here is that there are many known error cases:

  1. The user’s auth token might be invalid
  2. The user might send us 100GB of JSON (we should reject that)
  3. The JSON sent to us might be invalid
  4. and so on

Each of these has to be handled, with a sane http response going back to the user. I didn’t want to use anything like liberator (a clojure lib like web machine), because:

  1. performance: liberator is much slower than this code
  2. readability: this code has straight forward, sequential control flow (apart from errors being able to stop the chain at any point). This makes it easier to reason about what happens when.
  3. Also note that none of the stages know about each other, they’re only coupled through values.. This last factor means I can reuse the stages as I choose for e.g. another api handler (the api handler that receives deploy notifications looks very similar, just composes together a few different stages).

Error handling style is an engineering decision with several options for how you do it. Here’s a rough comparison of the styles of error handling I could have done here:

1. Exceptions

The typical option in most programming languages is to use exceptions, with a different type for each error response. Now it’s somewhat painful to define Exception subtypes in clojure, so I generally don’t go down this route.

Error handling is also a case where you should think about how the software is going to grow. Right now, Yeller’s api handler has 9 different stages, many of which can fail and should return a decent error code to the user. I see that might potentially grow in the future, so part of the error handling style to evaluate here is what it takes to add a new stage. Here’s what that looks like with exceptions:

  1. Define a new exception subtype
  2. Add your code for the stage
  3. Add a new catch to the toplevel and return the correct response

That’s three different places where the code has to change. Ideally there would be two per stage, one to write the stage, one to compose it into the toplevel. The latter change should be as small as possible, so that the toplevel code doesn’t explode in size as stages increase.

2. Conditionals and multiple returns

The next most common option is to use multiple return values and conditionals. Indeed, this is the only real error handling golang supports. So, what does adding a new potentially erroring stage look like in this style:

  1. Add a new if err != nil line to the toplevel
  2. Add an early return with the http code we want
  3. Add the possibly erroring code

That’s better, but it still requires a reasonable amount of code in two different places. It’s also very verbose, leading to functions that look like this one.

3. Monads and Macros

There’s that word.

I’m not going to talk about monads here at all, though the error handling style I like is the Either monad. Here’s what I want again:

One function composes the stages of the api handling function. Each stage can fail

(This thinking partially prompted by this tweet).

So I turn to abstracting the conditional style of error handling. Both monads in haskell and lisp macros let you abstract away the error handling conditionals and short circuiting in the failure case. Yeller’s implementation is done via a macro, here’s the source. Failure is handled by your “stage” returning a fail, which is a macro that unwraps to a function called to present the error to the client. Here’s what’s required now to add a stage:

  1. Write the stage code and put it in a function, call fail if this stage has failed
  2. Call the function from the toplevel handler

That’s it. The second change is very minimal, usually just one single symbol added to the AST.

So the real tradeoff here between this and using exceptions is where your list of error responses goes. With exceptions, they’ll likely go all in one function, with a long list of try/catch blocks. This actively hurts being able to grow the stage list, but it does let you quickly scan the error responses.

You can see a similar tradeoff with web routing: do all your routes go together in one file? Or do they get scattered across the codebase. Rails does the former, JAX-RS/Jersey/DropWizard does the latter.

Do it Hurt Performance?

So I think this is a nice abstraction. But what’s the performance impact?

Well, err->> is a macro, so most of its impact is at compile time (and only happens once). It has much the same performance as the conditional technique, except it’ll invoke the conditional on every single step rather than selectively. That’s fine for most uses though. It adds one extra allocation when the handler fails (wrapper the failure in a Failure) , but I care less about that case than making success be fast.

Currently Yeller’s api handler stands at 4ms for the 99th percentile latency. I think that’s fast enough for now.

Tradeoffs

Note again that this is a place where tradeoffs matter. If your toplevel code isn’t so sequential, or there’s a deep call tree or something, exceptions might be the better choice. Understanding the tradeoffs you are making is crucial.

This is a blog about the development of Yeller, the Exception Tracker with Answers.

Read more about Yeller here

Looking for more about running production applications, debugging, Clojure development and distributed systems? Subscribe to our newsletter: