Family Ties part 5: Exceptionally Similar
familyties
This is the fifth of a series of articles on what I’ve learned about Erlang (and Elixir) from writing Erl2ex, an Erlang-to-Elixir transpiler. This week we take a look at exception handling: how each language identifies different types of exceptions and how closely they relate.
A word about exceptions
Erlang and Elixir both provide exception handling facilities: you can throw exceptions when an unexpected condition is detected, and catch them further up the call chain. However, in most cases, this comes with a disclaimer:
Don’t handle exceptions. At least, not unless you have an extraordinary situation.
Dave Thomas’s book Programming Elixir emphasizes this by relegating the discussion of exceptions to an appendix, with a final subheading of “Now ignore this appendix”.
This is for good reason. The much-lauded fault tolerance of the platform is based on separating code from error handling, allowing errors to crash a process and employing supervisors to recover from them. While there are valid use cases for “traditional” exception handling, they are uncommon in Erlang and Elixir applications.
That said, a brief look at the exception handling facilities is instructive for understanding the relationship between the two languages.
Three exceptions in one try
Erlang defines three “classes” of exceptions, corresponding to three different use cases.
- Exceptions of class
error
generally denote runtime errors. These include pattern match failures, undefined function calls, and similar cases, including application-defined errors. - Exceptions of class
exit
are raised when a process is killed. - Exceptions of class
throw
may be raised and caught by your application code to perform a non-local return.
Elixir also recognizes those same three classes, so exception handling is interchangeable in the two languages. That is, exceptions thrown in either language can be handled in the other.
Both languages also share a similar set of language constructs for raising and catching exceptions. Each has a try
construct that pattern matches caught errors and provides cleanup capability. Each has a throw
construct for initiating non-local returns. (Erlang also provides an older standalone catch
construct that traps any exception and returns information about it, but it seems to have limited use, and Elixir does not provide a direct equivalent.)
There is one obvious difference, however. Elixir defines a set of explicit exception types, each associated with a module. Its try
construct can include a rescue
block that selects based on which exception type was raised:
Those types don’t exist in Erlang. Does this have a significant effect on Elixir exception handling overall?
Exceptionally complete
Erlang distinguishes different error types using atom values. A division by zero, for instance, will raise a badarith
error. These identifiers can be pattern matched in Erlang’s try-catch construct:
Elixir’s exception types are simply a slightly higher level representation of Erlang’s errror identifier, providing additional information about the error (such as a user-comprehendable message string). Corresponding Elixir code might look something like this:
The direct correspondence between the Erlang atom and the Elixir module is illustrated by the fact that you can convert the former to the latter yourself by calling Exception.normalize()
.
And indeed, while Elixir’s high level mechanism is convenient to use, it is actually optional. It’s a lesser known fact that Elixir code can also use Erlang-style catch blocks, matching against Erlang’s atom error identifier. The above Erlang code could be directly translated thus:
Note the use of a “two argument” catch clause. The Elixir documentation describes using catch
with a single argument to catch throws, but the construct also supports two argument catches where the first is the exception class. Incidentally, this also means the following catches are equivalent:
The documentation also covers the after
clause, which mirrors the corresponding clause in Erlang, allowing cleanup with side effects after the block has completed. What the documentation doesn’t currently mention is that an else
clause is also supported. This mirrors the of
clause in Erlang, which provides pattern matching and postprocessing of the result of the try. Here’s an example in Erlang:
And the equivalent in Elixir (using Erlang-style exceptions)
Exactly the same features are supported, and even the syntax is nearly word-for-word identical.
Where to go from here
As we’ve seen, despite Elixir’s new Exception types and behavior, the exception handling in the two languages is remarkably similar. Most constructs in one language have a direct analogue in the other, and it is possible to translate most Erlang exception handling pretty much word-for-word to Elixir. You can learn more from Erlang’s and Elixir’s reference documentation on their respective “try” constructs. While I was working on Erl2ex, discovering the similarities therein demonstrated to me how closely related the two languages truly are.
Next time, we’ll compare how Erlang and Elixir define and use guard clauses. Feel free to browse the index of articles in this series, and stay tuned for more on Erlang and Elixir’s family ties.