Graceful error handling in Elixir

Course correct your users with helpful error messaging

In classical piano repertoire, there’s a piece known as the “wrong note” etude — named that way because the piece is composed in such a way that the notes feel wrong to play. Usually, wrong notes sound dissonant and out-of-place, but the wrong note etude trains the pianist to embrace dissonance in order to bring out the piece’s main melody.

If you fancy a listen, Pollini’s interpretation of the wrong note etude is my favorite. (Photo by Tadas Mikuckis on Unsplash.)

You might think, “Great. So what does this have to do with error handling?” Well, to be honest it doesn’t. I just thought it added class to my blog post. 🐵

…but to make a loose analogy, a pianist is to a wrong note as a programmer is to an error message. An error message can be dissonant and unhelpful — much like an ill-placed sharp note — or it can be a cue for adjustment. Good error messages are course-corrective: they guide the user toward your software’s happy path, the way you intend the software to be used.

Error handling facilities in Elixir

We’ve established that good error messages correct, but what do we have to work with in Elixir?

Generally, Elixir prescribes two primary ways of returning errors from a function. You can return a tagged tuple, indicating {:ok, result} for a successful result, or {:error, reason} if something went wrong. You can also create a “bang” version of the same function for_example!/1, that simply returns the result or raises an error. Let’s examine both methods to see if either allow us to author the course-corrective error messages that we want.

Examining bang functions for gracefulness

Bang functions are convenient, since if you let a bang function’s raised error crash the process, you get a nice stack trace for free.

However, while stack traces are useful to developers as a debugging aid, they often appear as vague errors to end users of your software.

We can try rescuing errors to make them more useful:

This is better, but rescuing errors burdens the developer. If a function is expected to raise multiple types of errors, having to rescue all expected errors (and provide corrective error messages for each error) is brittle, and hard to extend and maintain. This is probably what people mean when they say checked exceptions in Java are bad.

On top of that, bang functions pollute your codebase. A function that calls a bang function is itself a bang function. With too many bang functions, it becomes difficult to trust any part of your codebase.

Although we can author error messages by rescuing errors and pattern matching, maybe there’s a less fragile way of doing so.

Examining tagged tuples for gracefulness

Tagged tuples offer more messaging control over bang functions. In particular, if the reason in your {:error, reason} tuple is an Exception struct, you get the equivalent of bang functions without the exceptional control flow:

Conveniently, Exception structs can be formatted in a uniform way with Exception.message/1, and you can re-contextualize Exception structs for further annotation:

Authoring messages with the errors package

With a small Elixir package of mine named errors, you can re-contextualize your Exception structs with less boilerplate:

Pass the wrapped error to IO.puts for a concatenated string of all your annotations:

Or retrieve a stack trace with IO.inspect. Note that this is mostly helpful for developers, not for your end users:

Conclusion

Graceful error handling is all about the user. Strive to guide the user toward your software’s happy path, via course-corrective error messaging. Program to an interface by returning Exception structs and using Exception.message/1, and author good messages by wrapping. Finally, use the errors package for help with wrapping errors and printing errors out.

I hope this post informs your future error handling efforts. 🙂


errors was inspired by Dave Cheney’s Go library of the same name.