Error Handling Styles For API Servers
This is a blog about the development of Yeller, The Exception Tracker with Answers
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:
- The user’s auth token might be invalid
- The user might send us 100GB of JSON (we should reject that)
- The JSON sent to us might be invalid
- 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:
- performance: liberator is much slower than this code
- 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.
- 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:
- Define a new exception subtype
- Add your code for the stage
- 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:
- Add a new
if err != nil
line to the toplevel - Add an early return with the http code we want
- 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:
- Write the stage code and put it in a function, call
fail
if this stage has failed - 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.