A hotshot teammate just pulled a miracle. Over the weekend, they built the API our customers have been promised for the past year, averting an existential crisis for the company. One problem: Only the happy path returns a response; all exceptional circumstances simply crash the computer. So, it’s up to us to productionalize the prototype by adding appropriate error handling, and we won’t sacrifice compile-time safety and re-usability along the way.
This post illustrates three different models of error handling:
- using continuation-passing style to return early, and
- monadic composition of
The terms error, failure, and (lower-case) exception will be used interchangeably throughout this post. (While a distinction can be useful, we will not need one here.) Capital Exception will refer properly to language-specific notions, such as Java’s dedicated
Exception class, where needed.
The code examples are in Scala, but the patterns should translate to most other languages that have first-order functions and generic type parameters. If your favorite language does not support Java-style exception handling—with a dedicated
Exception class and specialized keywords like
catch—we’ll see how to implement exception handling as user-defined types and functions in a type-safe way.
An Example Problem
We are to implement an API over HTTP which, for simplicity’s sake, only accepts POST requests. Users specify a resource in the path of their request, provide an auth token in their header, and the body of their request gets posted to the resource (which probably involves writing to a database, but we’ll leave the precise meaning undefined).
Our domain classes consist of
Resources. Conceptually, a
User is identified by their auth token, and a
Resource is resolved by its path:
Next, we need to apply the request body to the resource as the user:
Of course, we still need to return an appropriate HTTP response even when we cannot complete all of those tasks. The current prototype returns a response only when all the right conditions are met, and halts otherwise.
Here’s the project spec, complete with nine canned responses that cover the various things that might go wrong. Take a moment to read over the canned responses to get an idea of the kinds of failures we have to handle:
And here we have the prototype, which returns
success when everything goes right, and halts when anything goes wrong:
(Above and in what follows, identifiers surrounded by
` denote code blocks that we could implement in principle, but will leave undefined for the purposes of this post.)
What Exactly Counts as “Exceptional”
Examining the prototype’s
handlePost method, we see the outline of a simple data-processing pipeline:
- Check that the method is POST,
- Check that the body is not empty,
- Get the appropriate
- Grab the appropriate
- Execute, using the
We use the word “pipeline” intentionally, because subsequent steps rely on the successful completion of prior steps. When one step fails, in order to avoid undefined behavior or crashing the computer, we need to short circuit processing, escape from the pipeline, and jump to designated error-handling code. That is why we call such failures exceptions. They represent exceptions to the intended processing pipeline, and they require us to skip the remainder of the computation.
This is a common problem in control flow, and the idiomatic Java way to solve this problem is by
catching them later. The
Exception class and the keywords
catch do exactly the kind of short circuiting and redirecting we need.
Let’s refactor the prototype’s
getUser method to throw an
Exception instead of crashing the computer:
We enumerate the kinds of failures we may have as case classes. Above the method signature, we replace human-readable comments with compiler-readable
@throws annotations. Instead of crashing the computer, we throw, which allows us to catch later, but uncaught Exceptions will still crash the program.
We’ll need to refactor
handlePost to catch the exceptions thrown by
getUser. We wrap the method body in a
try block and add a
catch block after:
In each case, we simply return the appropriate canned response. Notice
handlePost itself does not throw. Our helper methods can throw Exceptions, but we want to guarantee that
handlePost returns a
Response, so we need to catch every possible Exception that we’re aware of. The buck stops at
Let’s see what the whole pipeline looks like refactored using
Analysis of Using Exceptions
Where the prototype incorrectly handled failures, this new refactor is correct, but our code exploded: It’s over twice the size of the prototype. This might not sound like a huge problem, but bear in mind that in a production environment, every line of code is an ongoing maintenance burden, so it really pays in the long run to keep your code clean.
In addition to being verbose, the helper methods are somewhat unusable except in this context, because in order to reuse them, one must anticipate that they will throw and catch appropriately. For example, while we’ve been extra careful and made sure that our call site,
handlePost, catches everything that might get thrown, we might anticipate a team member extending this API to handle GET and DELETE requests. They’d probably want to reuse some our helper methods, but they’ll have to anticipate these throws, and they’ll get no help from the compiler when they do.
So, the code is now bloated and the helper methods are unsafe, making them hard to reuse. Let’s try to figure out why that is.
First of all, the helper methods are unsafe precisely because we throw, and thrown Exceptions are unchecked in Scala. (Thrown Exceptions might as well be unchecked in Java in light of all the ways people have learned to fool the compiler.) If we want compile-time static checking that our methods won’t crash the computer when they are reused, we simply can’t be throwing Exceptions.
Second, the code explosion is mostly due to repeating ourselves. Each individual error type gets represented five separate times in our program:
- Once when we create a case class extending
- Once when we annotate our method,
- Once at the site where we encounter the error and throw,
- Once when we catch the
- Once when we map the exception to the appropriate canned response.
Our end goal with this program is to ensure that an appropriate
Response is created and returned. If we can find a way to cancel the remainder of the computation and exit early with the appropriate response at the point of failure, then we can cut out the middle step that involves throwing and catching.
throw in order to skip the remainder of the computation and jump to the
catch block, were we create an appropriate
Response. If we pass the remainder of the computation into each method as an argument, then we can skip it simply by not calling it.
Doing this is a lot more straightforward than it sounds. Imagine we have a computational pipeline where the end result is a value of some type
C. Suppose we have some method that is part of our pipeline that takes a value of type
A and returns a value of type
B. Here’s a template for how to refactor that method:
Instead of returning a
B, we accept a function that eats a
B and continues the computation, resulting in a
C that our method will then return. It’s called continuation-passing style because we pass in a continuation, a function that represents the rest of the computational pipeline.
If you’ve done any Node.js programming, you’ve likely used and even written functions that have this shape. If not and you’d like to get a feel for using and writing functions in this style, I recommend you take a few hours one afternoon and complete the exercises in Learn You The Node.js.
The template we have doesn’t show how to deal with failures. Here’s a slightly fuller template:
While it might look somewhat roundabout, the advantages of using continuation-passing style here are that the methods are now compile-time safe (making it easier to reuse without mistake) and shorter (creating less of a maintenance burden) with fewer top-level abstractions (resulting in less cognitive overhead).
The key to interpreting code like this is to think of
subroutine as providing a hypothetical
pipeline, we call
subroutine, which produces a
B that we then name and go on to process. Try to think of the call to
subroutine as an assignment where the name is on the right instead of the left. Something like “
subroutine(a), and then …”
To get an idea of how to use continuation-passing style, let’s refactor
Instead of returning a
Resource we accept a continuation and we return a
Response. Instead of throwing an Exception in case of failure, we ignore the continuation and return
Let’s examine the corresponding change in
No more catching Exceptions and mapping them to their corresponding
getResource will return the appropriate
Response in case it fails. Notice that
handlePost does not take a continuation for the same reason that its counterpart method in the exception-passing version above does not throw.
handlePost is the end of the (pipe)line.
Here’s the whole program, refactored to use continuation-passing style for exception handling instead of idiomatic-Java Exception throwing:
We see the code is much shorter, mostly because we’re not repeating ourselves so much, and we have the additional benefit of stronger compile-time guarantees that our code is not broken, making it easier (in at least the Correctness dimension) to reuse our helper methods when we inevitably extend this API six months from now. Also, we handle failures at the point of failure, instead of some far-off place in our code, which in this case I feel is a benefit but does admittedly lead to tighter coupling. (We could regain flexibility by refactoring our methods so that the caller supplies
Response values to use for the various failure cases. E.g., pass
noResource(path) in as an argument to
Writing in continuation-passing style makes it easier to reuse our helper methods in the following sense: We have rigged their signatures so that there’s no way for us to forget to handle failures. However, passing around continuations can be a bit awkward, putting an extra burden on us at the call site. In that sense, these helper methods are a little bit harder to reuse. Continuation-passing style happens to be one of the most-versatile tools in a programmer’s toolbox. Using them merely for error handling is kind of like swatting a fly with a wrecking ball.
Either class provides an abstraction that is a little more focused in its scope, making it easier to use. The
Either class provides short-circuit, pass-through semantics much like
catch does, but
Either has the advantage of being a concrete data structure, representing the control flow as a first-class value.
That was a bit long-winded, so let’s take a look at how
Either achieves short-circuit, pass-through logic. (The implementation is simplified for the purposes of this post.)
We see the pass-through logic in
bindEither. If an
Either value is a
Left value, applying
Either#flatMap will safely ignore the supplied functions, preserving the
Left value along the way. Here’s
pipeline from the example above refactored to either-passing style:
Instead of returning a
subroutine returns either a
C (the exit-early case) or a
B that we can consume later in
pipeline uses Scala’s
yield syntax to destructure either values and to chain successive computations based on those values. Each
<- gets compiled into a call to
flatMap (giving us the desired short-circuit semantics), and
yield gets compiled into a call to
In software as in life, nothing is free. Calling
subroutine is easier here than it was in continuation-passing style, but the cost is that
pipeline must consume the
Either produced by the
yield block, branching on its possible cases. The branching is trivial, as both cases contain a
C at this point, but we still need to do it unfortunately. One thing we can do to make it a bit nicer is to use
Since both cases contain a
C, we simply want to pass that
C forward unchanged:
As we refactor our program to use either-passing style, we will use the type of the end-result of our pipeline for the left generic parameter of
Either. In other words, we’ll want to work with
Either[Response, _] values. Let’s take a look at
getUser written in either-passing style:
This code is fairly readable, compile-time safe, and easy to reuse. But we can still do better: Particularly, the repeated if-else is a bit clunky. We can factor this idiom out into its own method as an effect, a method that returns an
Either[Response, Unit]. Then we can use
yield notation to chain effects. We need our effect to return
Left(`some appropriate response`) (thus aborting the remainder of the computation) if some boolean condition is met, so we’ll name our effect
failIf and it will take a
Boolean and a
Response. Let’s see how this is done:
In fact, there’s nothing special about
Response in that method, so we can make this effect more reusable by using a generic type parameter instead of hard-coding the
Now we have a generic method that we can call in
yield blocks at any place where we’d like to return early. Let’s see it in action:
Since the result of
failIf is the unit value
(), we’re safe to throw it away—thus the
_ assignments. We’re calling
failIf for its effect, not for its result.
This is still pretty clunky, and partly because of that first assignment for
req.header.get("Authorization") returns an
Option[String]. If we can turn that
Option[String] into an
Either[Response, String], then we can write the whole method in
yield notation and make it much less clunky. In order to turn an
Option[String] into an
Either[Response, String], we’ll need to supply a
Response to create a
Left value in case the option is empty. The method that does this is
Let’s refactor the rest of our program to use either-passing style instead of exception-passing style or continuation-passing style:
Analysis of Continuations and Eithers
I avoid throwing exceptions in my own code. I prefer patterns that preserve compile-time safety, as it makes the code easier to test and easier to reuse (this is what’s usually meant by “easier to reason about”). Both continuation-passing style and either-passing style preserve compile-time safety, making it harder to write code that is broken by design.
Between the two, I find that either-passing style is a bit easier to fit into larger designs: The methods are easier to invoke, since you don’t need to provide a continuation, and their use is more obvious, since they return a concrete data structure that can be assigned or passed around. To use continuation-passing style effectively you may need to exercise a lot of forethought.
Either abstracts a single aspect of control flow: short-circuit control flow. For some perspective, consider that
Future abstracts asynchronous control flow,
List can be used to abstract non-deterministic control flow, and
Stream can be used to abstract parallel control flow. Continuation-passing style, on the other hand, abstracts control flow. Period. It can be employed to great effect to create first-class representations of control-flow features that might otherwise require specialized keywords and language semantics. In effect, you can add almost arbitrary functionality to your language using continuation-passing style thoughtfully.
In the end, the choice between either-passing style and continuation-passing style is largely a matter of taste. What is more readable to one person may be less readable to another person for instance. The key take-away is that both idioms allow us to avoid writing methods that throw, making it easier to reuse our code and write correct code.
Thanks to @anthony__brice for helpful corrections and suggestions.