Erlang from the ground up

Errors: try, catch and throw

Errors in Erlang are values with a class. try runs code that might fail, catch says what to do about each class, and throw is a signal you can raise and catch yourself.

Part 11 of 12

Advanced18 min readBack to Courses

Errors have a class and a reason

When an Erlang expression cannot be evaluated it raises an exception made of a class and a reason. Dividing by zero raises class error with reason badarith. Any expression can raise a throw with any reason you like. Process failures arrive as class exit. The class is what a handler matches on first.

An exception that nothing catches stops the function that raised it and everything that called it. The output then shows a stack trace, which is a truthful report of what happened rather than a guessing game.

try with of and catch

try takes the code that might fail. After the keyword of come the patterns for the normal answer, exactly like a case; after catch come clauses of the shape Class:Reason, one for each kind of failure being handled. The whole expression is closed with end.

The of part is optional. try Expr catch ... end is enough when only the failure matters and the normal answer can pass through untouched. When both parts are written, the of patterns see only successful values: a raised exception never lands in them.

A catch clause whose reason is not the one that arrives does nothing, so exceptions travel outward until something handles them or the program stops. Handling an exception is a decision, which is why the enabled class is named in the code rather than being left to a broad catch-all.

throw for your own signals

throw(Reason) raises a value of class throw. It is not an error in the sense of a bug; it is the ordinary way to leave a deep function early with an answer the caller expects. The caller writes throw:Reason in a catch clause and gets the value back.

The pair is small and worth keeping that way: throw a reason where the decision is made, catch the reason where the decision can be acted on. A function that throws and instantly catches its own throw is only confusing itself.

erlang:error(Reason) raises class error with your own reason, which is the right choice for a broken precondition rather than an expected outcome. throw is for control flow the caller is meant to catch; error is for a bug or an impossible state, and a handler that catches it should usually re-raise rather than continue.

The distinction is a convention, not a rule the runtime enforces, but it keeps the two uses readable: a throw in a parser says that the input is not for it, and an error says that the parser itself is wrong.

One division that fails, one signal that is caught

-module(prog).
-export([main/1]).

main(_) ->
    io:format("~p~n", [safe_div(10, 2)]),
    io:format("~p~n", [safe_div(10, 0)]),
    io:format("~p~n", [sign(-1)]).

safe_div(A, B) ->
    try A / B of
        Result -> {ok, Result}
    catch
        error:badarith -> {error, bad_number}
    end.

sign(N) ->
    try
        if N < 0 -> throw(negative); true -> N end
    catch
        throw:Reason -> {thrown, Reason}
    end.

Two failures, two answers

Run it and check that the good division still returns a plain answer. Then call `safe_div(0, 0)` and add a catch clause for a reason of your own.

Output
Click "Run" to execute your code

A catch clause naming only a class, such as error:_, swallows every error of that class and hides real bugs. Name the reason you expect, and let everything else travel outward where the trace can be read.

A failure has a class, a reason and a stack

Every exception carries three things: a class, a reason and a stacktrace. The class says where the failure came from. error is a runtime fault such as badarg or badarith; exit is a process being asked to stop; throw is a signal your own code raised with throw/1. The reason is the detail, and the stack says which calls were active.

A catch clause can match all three, as in error:badarith, or omit the class and match only throws. Naming both parts is what lets a handler distinguish “the input was not a number” from “the process was shut down” without inspecting the reason by hand.

after runs whether the try succeeds or fails

try ... after ... end is the cleanup form. The after block runs on the normal path, on a caught exception, and on an exception that is still travelling outward. That makes it the place for work that must happen either way, such as closing a file or printing a final line.

The value of the whole expression is still the value of the try body or the chosen catch clause; after does not replace it. If a handler needs to pass a failure on, erlang:raise(Class, Reason, Stacktrace) re-raises the original with its trace rather than inventing a new error.

Three habits make error handling worse. A broad error:_ clause catches bugs you wanted to see. The old catch Expr form flattens every failure into a value and loses the class, so use try when the distinction matters. And cleanup written after the try block is skipped when an exception escapes; put it in after instead.

Try these next

  • Make parse return a different reason for an empty string and for a word.
  • Add an after line to a function that succeeds and one that fails, and confirm it prints both times.
  • Catch a throw and an error in the same function with two separate clauses.

Parse a number and clean up afterwards

Run it and check that the bad text gives an answer instead of stopping the program. Then change the cleanup line in `signal` and confirm it still prints for a negative number.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does 10 / 0 raise?