Exceptional: Freedom from {:error}s

An error handling library (and philosophy) for Elixir

Brooklyn Zelenka
The Monad Nomad
6 min readSep 6, 2016

--

TL;DR

Exceptional is an Elixir library providing helpers for working with exceptions. It aims to make working with plain old (unwrapped) Elixir values more convenient, and to give full control back to calling functions.

Reviewing the Current State of the Art

Coming out of the Erlang/OTP tradition, Elixir makes heavy use of a tagged status tuples: {:ok, value}, {:error, reason}, or some {:other_tag, stuff}. This makes perfect sense in the context of communicating across a network, where you want to keep your metadata light, easily streamable, and there are few states that a process can respond with.

One of the nice things about tagged status tuples is that they force you to handle the branching condition. This generally falls into a handful of very common patterns. For {:ok, _}, this is almost always to extract the value and continue. For {:error, _}, we tend to either raise, pass the error tuple up to the caller for handling, or retry.

Exceptions

In case you’re not familiar with them, exceptions are regular structs that implement the Exception behaviour. The Kernel module (default imports) has a macro for defining them (defexception), and they exist throughout the existing system (ex. Enum.OutOfBoundsError).

Exceptional’s Philosophy

Exceptional takes a slightly different view. While keeping things lightweight for inter-process communication makes perfect sense, it can sometimes be frustrating to not have enough information (or control), especially when running inside a single process.

One of Elixir’s greatest strengths is developer experience. Like Ruby and various Lisps before it, having a friendly syntax, plenty of convenience functions, and great documentation really drives adoption — probably more than the performance advantage of Phoenix over Rails. After all, Erlang has been around for 30 years, and hasn’t seen this level of mass-appeal.

While the overall experience is pretty fantastic, idiomatic error handling can be a little bit cumbersome at times. I don’t know about you, but I have consumed library APIs (and written functions) with multiple {:error} tuple lengths, like this:

These situations can quickly become pattern-matching thorns-in-your-side. The temptation quickly becomes to just give up and fail fast: raise an error, and either rescue it up the line or let the process die. But this is functional programming! We’re supposed to be explicit about things! Many (though not all) errors are supposed to be handled gracefully — even in Erlang/Elixir. (Hence the {:error, reason} in the first place).

Optimistic Piping, or “Just Say It”

Being forced to handle the branching condition of tagged status tuples is great for building robust code, but a raw value is implicitly {:ok}. On the other hand, we should be able to just state a value without attaching additional metadata that we need to manually unwrap (or that can come out of sync with the tag). We can still get the branching behaviour by expressing error states as values, and continuing to include them in our typespecs.

Pure Control, Inverted Responsibility

Purity is a central theme of functional programming — and for good reason! It keeps code crystal clear, forces us to be aware of functions that can blow up, and encourages us to handle all possible cases, all to create ultra-high quality software. While Elixir is not a totally pure language (you can do I/O anywhere), it’s still good practice that leads to better software. Falling out of this principle: the caller should have the power to decide what to do with an error state, inverting the responsibility of alerting or handling to the caller.

I don’t know how you were inverted
No one alerted you
~ The Beatles, “While My Guitar Gently Weeps”

This makes a great deal of sense: a function that calls a dangerous function (something capable of raising) is itself dangerous. The possibility of blowing up propagates all the way up the call chain. Further, if you want to render a dangerous function safe, you have to wrap it in a try/catch/rescue. Not all libraries provide a non-bang version of their API, forcing you down this path.

As you’ll see below, using visually-distinct operators and typespecs help us understand the error-able portions of our code, delegating the responsability of handling or raising exceptions to the caller, leading to better code reuse and fewer unexpected errors.

Consistency

{:error} tuples generally follow the pattern {:error, reason}, but there’s a bunch of variants (see above). For instance, if you want to return the offending data, you have to change the tuple length ({:error, “boom”, object}), and it will no longer pattern match as the original tuple.

Exceptions are structs, which in turn are maps. Maps don’t have a dependency on value order or length, so pattern matching on exception values is much less brittle than matching on tuples.

Clarity

Why not use a pattern like try/catch? While try/catch is well-understood, it has a tendency to hide information. By being explicit, we keep our code clean, clear, contextual, and easy to follow.

Better Living Through Context

As programmers, we spend a lot of time holding context in our heads. As we move up from details to the top-level API, composed functions generally have more abstraction and greater meaning for final actual objective. Why not delegate as much of that as possible to our programs with errors that match this richer semantic level?

Yes, there are stack traces for when an error gets raised, but stepping through a stack looking for the cause of a generic error is tedious and sometimes error-prone, and makes for some terrible log messages. We often don’t surface information to users, other than some cryptic error message or an unannotated 422 or 500.

Passing exception values up the line allows us to recontextualize exceptions to add information for that particular context. As a (contrived) example, let’s say that you have an ebook application with a title fuzzy search feature. ** (Ecto.MultipleResultsError) “expected at most one result but got 7 in query” doesn’t make for a very detailed log message, and may not be helpful when surfaced as a 404. The problem could be the query itself (dev error), or the user may have asked for bad information (user error).

In context, why not ask the user to clarify their selection? This isn’t so much an irrecoverable error as an incomplete request.

What’s that plug_status field, you ask? In a Phoenix (or Plug-enabled) app, if/when this exception gets raised, it’ll be treated as a 422, and can be handled more gracefully than a plain unannotated 404/422/500.

The Exceptional Library

Below are some examples to give you a general sense of how Exceptional works. All operators have named function equivalents. While the API is relatively small and focused, it works really nicely in a number of scenarios, especially in Plug applications where recontextualization really shines.

Example Set Up

Make sure that you understand this example, because it’s going to be this example for the rest of the article:

Short Circuit

How do we abstract out the branching conditions? Well, we have a few options. The simplest of which is a “short circuit”: if an Exception struct is encountered, just skip over the next function and return it, otherwise continue into the given function.

(Gee, wouldn’t it be nice if there were some way to compose functions that didn’t require that extra named step variable when we don’t need it?)

Caller Raises

A nicety of delegating effect control and tracking error states as values is that the caller can decide what to do with an Exception. In this case, we’re going to have it raise the exception itself. This means no more need for bang variants: Exceptional has an operator that asserts that something will either be a happy value, or it’ll raise the exception here!

Manual Branching

If you want to write your own Exception control functions, you don’t need to be forced to do all the plumbing yourself. There’s a handy if_exception function to help you, which comes in two flavours:

Compatibility: Make Things {:ok}

We still need to work in a system that otherwise expects tagged tuples. to_tagged_status (and its aliases) are very easy ways to convert into the tuple representation:

Summary

Well, there you have it! A simple API for another take on error handling with a focus on clarity, consistency, context, and freedom from unwrapping all those {:ok} tuples ;)

Don’t forget to check out the docs! I’m always open to feedback, and if you have any feature requests, don’t hesitate to add an Issue, and contributions are always welcome!

--

--