100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Error Handling in Erlang

Erlang handles failure through a distinctive combination of try/catch exception handling and the 'let it crash' philosophy, where supervisors, not defensive code, are the primary safety net.

Data & RecordsIntermediate10 min readJul 10, 2026
Analogies

Three Kinds of Exceptions

Erlang has three distinct classes of exception, all raised and caught through the same mechanism but carrying different intent: error/1 signals a genuine bug or unexpected condition (like a failed pattern match, which itself raises a {badmatch, Value} error, or a badarith, badarg, or function_clause error the runtime generates automatically), exit/1 signals that a process wants to terminate, whether normally or abnormally, and throw/1 is a non-local return mechanism used for ordinary control flow, such as escaping early from a deeply nested computation.

🏏

Cricket analogy: A batter given out for hitting their own wicket (a genuine mistake, like error/1), a player retiring hurt mid-innings (a deliberate exit, like exit/1), and a captain calling for a strategic timeout to reset momentum (a planned interruption, like throw/1) are three distinctly different reasons play might stop, mirroring Erlang's three exception classes.

try...catch and the catch Operator

The try Expr of Pattern -> Body catch Class:Reason:Stacktrace -> Handler end construct is the modern (OTP 21+) way to both run code and handle any of the three exception classes it might raise, where Class is bound to error, exit, or throw so a single catch clause can distinguish which kind of exception occurred, and the optional Stacktrace variable, retrievable this way instead of the older erlang:get_stacktrace/0, lets you log or re-raise the original error context. The older, terser catch Expr operator is still common for simple cases: it evaluates Expr and, if an exception occurs, returns {'EXIT', Reason} for error and exit, or Reason directly for a throw, but because it collapses all three classes into ambiguous return shapes, catch is best reserved for situations where you genuinely don't care which kind of exception happened.

🏏

Cricket analogy: A stump microphone that separately tags an umpire's decision, a player's own comment, and crowd noise into three distinct labeled channels is like try...catch's Class:Reason:Stacktrace distinguishing error, exit, and throw, whereas an old single unlabeled broadcast feed that just says 'something happened' is like the terser catch operator's ambiguous {'EXIT', Reason} shape.

erlang
-module(error_demo).
-export([safe_divide/2, demo/0]).

safe_divide(A, B) ->
    try A / B of
        Result -> {ok, Result}
    catch
        error:badarith:Stacktrace ->
            {error, {badarith, Stacktrace}};
        error:badarg ->
            {error, badarg}
    end.

demo() ->
    {ok, 2.5} = safe_divide(5, 2),
    {error, {badarith, _}} = safe_divide(5, 0),

    %% "Let it crash": a worker that just dies on bad input, trusting a
    %% supervisor to restart it, instead of defensively handling every case.
    process_flag(trap_exit, true),
    Worker = spawn_link(fun() -> 1 = 2 end),
    receive
        {'EXIT', Worker, Reason} ->
            io:format("Worker crashed as expected: ~p~n", [Reason])
    after 1000 ->
        io:format("No exit received~n")
    end.

Since Erlang/OTP 21, the recommended catch form is Class:Reason:Stacktrace, which binds the stacktrace explicitly in the catch clause; the older erlang:get_stacktrace/0 function it replaced was error-prone because the stacktrace could be silently clobbered by intervening code before you read it.

The 'Let It Crash' Philosophy

Erlang's most distinctive error-handling idea is 'let it crash': rather than wrapping every risky operation in defensive try...catch blocks, idiomatic Erlang code often lets a process simply die when it hits an unexpected condition, trusting a supervisor to notice the crash and restart the process into a known-good state. This works because Erlang processes are cheap and isolated, a crash in one process cannot corrupt another process's memory, and restarting a process from scratch is usually both simpler and more reliable than trying to enumerate every possible failure mode and defensively code around each one, especially for transient failures like a database connection blip that a fresh process will simply not encounter on its next attempt.

🏏

Cricket analogy: A T20 franchise doesn't rebuild its entire strategy around one dismissal; it sends the next batter in fresh rather than trying to defend against every conceivable way a wicket could fall, the same 'restart rather than defend everything' philosophy 'let it crash' embodies by trusting a supervisor to restart a failed process cleanly.

Reach for try...catch only around operations whose failure you can meaningfully recover from right there (like a malformed external input you can reject with a clean error tuple), wrapping broad swaths of business logic in catch-all try...catch blocks defeats 'let it crash' and tends to hide bugs that a supervisor restart would have surfaced and fixed cleanly.

Linking, Monitoring, and Supervisors

Supervision trees are how 'let it crash' becomes a real fault-tolerance strategy rather than just chaos: a process can link/1 to another so that if either one crashes abnormally, the linked process receives an exit signal too (which, by default, kills it in turn, propagating the failure), or it can set process_flag(trap_exit, true) to convert those exit signals into ordinary messages it can inspect and react to instead of dying, this is precisely the mechanism a supervisor behavior uses internally to notice when a child process has crashed and restart it according to a configured strategy (one_for_one, one_for_all, rest_for_one), without the programmer having to hand-roll any of that detection and restart logic themselves.

🏏

Cricket analogy: A team's fielding chain reacts as one unit, if the wicketkeeper misses a signal the slip cordon adjusts automatically, mirroring how linked Erlang processes propagate a failure signal, while the vice-captain (like a trap_exit process) absorbs bad news calmly instead of reacting instinctively.

  • Erlang has three exception classes, error (bugs/unexpected conditions), exit (process termination), and throw (non-local control flow), all raised and caught through the same mechanism.
  • try...catch (with the modern Class:Reason:Stacktrace form) lets you handle each exception class distinctly; the older catch operator collapses them into an ambiguous {'EXIT', Reason} or bare Reason.
  • 'Let it crash' means idiomatic Erlang often skips defensive error handling in favor of letting a process die and letting a supervisor restart it into a known-good state.
  • This works because processes are cheap, isolated, and share no memory, so one process's crash cannot corrupt another's state.
  • link/1 propagates exit signals between linked processes by default; process_flag(trap_exit, true) converts those signals into ordinary messages a process can react to.
  • Supervisor behaviors use linking and trap_exit internally to detect a child's crash and restart it according to a configured strategy (one_for_one, one_for_all, rest_for_one).

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ErlangStudyNotes#ErrorHandlingInErlang#Error#Handling#Erlang#Three#ErrorHandling#StudyNotes#SkillVeris