Daniel Azuma

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.

Crash or crash not

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:

# The Elixir "rescue" block has no direct analogue in Erlang.
try do
  raise "ouch!"
rescue
  e in RuntimeError ->
    Exception.message(e)
end

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:

% A simple exception catch in Erlang
try
  1 + a
catch
  error:badarith ->
    "bad arithmetic expression"
end

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 Elixir equivalent using high-level exception handling
try do
  1 + :a
rescue
  e in ArithmeticError ->
    Exception.message(e)
end

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().

# The arguments are the exception class and the Erlang identifier.
iex(1)> Exception.normalize(:error, :badarith)
%ArithmeticError{}

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:

# The Elixir equivalent using Erlang-style exception handling
try do
  1 + :a
catch
  :error, :badarith ->
    'bad arithmetic expression'
end

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:

# Both catch clauses would catch a throw (so only the first is invoked)
try do
  1 + :a
catch
  x         -> "Caught #{x}"
  :throw, x -> "Caught #{x}"
end

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:

% Erlang example demonstrating several "try" features
funny_delete(N, D) ->
  try
    N / D
  of
    0.0 -> "nothing much!";
    1.0 -> "the same!";
    Q -> Q
  catch
    error:badarith ->
      "uwotm8?"
  end.

And the equivalent in Elixir (using Erlang-style exceptions)

# Equivalent Elixir function demonstrating "try" features
def funny_delete(n, d) do
  try do
    n / d
  catch
    :error, :badarith ->
      'uwotm8?'
  else
    0.0 -> 'nothing much!'
    1.0 -> 'the same!'
    q -> q
  end
end

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.

Dialogue & Discussion