Exceptions in Erlang

Exception

In a programming language, an exception is something that could be generated when the system is behaving outside the normal execution path. An exception is mostly an error. In lots of programming languages, developers use exceptions as a meaningful information to do something or not.

For example, while reading a file, an exception can be generated because the file does not exists. The developer may choose to catch the exception and display a popup to ask the user to choose an other file, or the developer may not catch the exception because the file must have been there and if it's not there it's because something wrong is happening but the developer has no clue about what to do, so the best solution is to let the system crash (as opposed to try to do something and maybe enter in an inconsistent state).

Exceptions in Erlang

In Erlang there are exceptions too.

exceptions.erl

-module(exceptions).
-compile([export_all]).

run() ->
  io:fwrite("Test exception 1 starting...~n"),
  exception1(),
  io:fwrite("Test exception 1 finished.~n"),
  ok.

exception1() ->
  erlang:foo().

Running the function run will run exception1 which throws an exception (the function foo does not exist in module erlang) and is not catched by run (so the second fwrite is not displayed):

$ erl -s exceptions run
Erlang (BEAM) emulator version 5.6.2 [source] [smp:2] [async-threads:0] [kernel-poll:false]

Test exception 1 starting...
{"init terminating in do_boot",{undef,[{erlang,foo,[]},{exceptions,run,0},{init,start_it,1},{init,start_em,1}]}}

Crash dump was written to: erl_crash.dump
init terminating in do_boot ()

Types of exceptions

In Erlang there are 3 kinds of exceptions that can be generated:

  • normal exceptions, user generated (throw(Reason))
  • errors, something is going really wrong, should not be catched (erlang:error(Reason))
  • exit, used to terminate current process (exit(Reason))

Catching an exception

  • Catching an exception with catch:
run() ->
  io:fwrite("Test exception 1 starting...~n"),
  Result = (catch erlang:foo()),
  io:fwrite("Test exception 1 finished: ~p~n", [Result]),
  ok.

Calling run displays:

Erlang (BEAM) emulator version 5.6.2 [source] [smp:2] [async-threads:0] [kernel-poll:false]

Test exception 1 starting...
Test exception 1 finished: {'EXIT',
                               {undef,
                                   [{erlang,foo,[]},
                                    {exceptions,run,0},
                                    {init,start_it,1},
                                    {init,start_em,1}]}}

…and the process is still alive.

  • Catching an exception with try … catch. The full syntax is something like this:
try erlang:foo() of
  Any ->
    Any
catch
  error:Reason ->
    io:fwrite("Error reason: ~p~n", [Reason]);
  throw:Reason ->
    io:fwrite("Throw reason: ~p~n", [Reason]);
  exit:Reason ->
    io:fwrite("Exit reason: ~p~n", [Reason])
after
  io:fwrite("Doing some stuff no matter what happened.~n")
end.

try executes the given function and return it's value (which can be pattern matched) if everything's OK, if an exception is generated it goes in the the matching catch clause. In any case, it then goes inside the after block (similar to finally in Java).

Running the previous code output:

Error reason: undef
Doing some stuff no matter what happened.

(Calling a function that does not exists throws an error)

As everything in Erlang, try … catch returns a value (the executed function, the return the matching clause or the return of the matching exception clause).

Examples

Let see more examples.

exceptions.erl

-module(exceptions).
-compile([export_all]).

run() ->
  run(1, no_exception, 'catch'),
  run(2, no_exception, 'try'),
  run(3, 'throw', 'catch'),
  run(4, 'throw', 'try'),
  run(5, 'exit', 'catch'),
  run(6, 'exit', 'try'),
  run(7, 'error', 'catch'),
  run(8, 'error', 'try'),
  ok.

run(ID, Exception_type, Handling_type) ->
  io:fwrite("~p) Generating ~p, handled with ~p.~n", [ID, Exception_type, Handling_type]),
  Fun = fun() -> exception(Exception_type) end,
  Result = execute(Handling_type, Fun),
  io:fwrite("~p) Result: ~p~n", [ID, Result]).

exception(no_exception) ->
  ok;
exception('throw') ->
  throw("Throwed exception");
exception('exit') ->
  exit("Exited");
exception('error') ->
  erlang:error("Error generated").

execute('catch', Fun) ->
  (catch Fun());
execute('try', Fun) ->
  try Fun()
  catch
    Error:Reason ->
      {Error, Reason}
  end.

Output:

1) Generating no_exception, handled with 'catch'.
1) Result: ok
2) Generating no_exception, handled with 'try'.
2) Result: ok
3) Generating throw, handled with 'catch'.
3) Result: "Throwed exception"
4) Generating throw, handled with 'try'.
4) Result: {throw,"Throwed exception"}
5) Generating exit, handled with 'catch'.
5) Result: {'EXIT',"Exited"}
6) Generating exit, handled with 'try'.
6) Result: {exit,"Exited"}
7) Generating error, handled with 'catch'.
7) Result: {'EXIT',{"Error generated",
                    [{exceptions,exception,1},
                     {exceptions,execute,2},
                     {exceptions,run,3},
                     {exceptions,run,0},
                     {init,start_it,1},
                     {init,start_em,1}]}}
8) Generating error, handled with 'try'.
8) Result: {error,"Error generated"}

An interesting thing we can see here is that, in case of an error, catch get a stack trace which can be very useful for debugging but try … catch does not get it.

I prefer try … catch syntax (and it's the recommended way to catch exceptions because you can choose what kind of exceptions you want to catch, catch catches everything) but it's regrettable that it does not return the stack trace.

You can use erlang:get_stacktrace but it returns the stack trace from where you are calling it. If the exception is generated deep inside the function you are calling, get_stacktrace does not gives the root cause of the exception.

Having a stack trace is very useful but it make things a bit slower. I made a simple benchmark:

bench() ->
  Throw_fun1 = fun(_) -> (catch exception('throw')) end,
  Error_fun1 = fun(_) -> (catch exception('error')) end,
  Throw_fun2 = fun(_) -> try exception('throw') catch Error:Reason -> {Error, Reason} end end,
  Error_fun2 = fun(_) -> try exception('error') catch Error:Reason -> {Error, Reason} end end,
  Seq = lists:seq(1, 100000),
  timer:sleep(1000),
  {Time_throw1, _} = timer:tc(lists, foreach, [Throw_fun1, Seq]),
  {Time_error1, _} = timer:tc(lists, foreach, [Error_fun1, Seq]),
  {Time_throw2, _} = timer:tc(lists, foreach, [Throw_fun2, Seq]),
  {Time_error2, _} = timer:tc(lists, foreach, [Error_fun2, Seq]),
  io:fwrite("Throw (catch): ~p micro seconds~n", [Time_throw1]),
  io:fwrite("Error (catch): ~p micro seconds~n", [Time_error1]),
  io:fwrite("Throw (try): ~p micro seconds~n", [Time_throw2]),
  io:fwrite("Error (try): ~p micro seconds~n", [Time_error2]),
  ok.

Results (for 100 000 calls):

Throw (catch): 73920 micro seconds
Error (catch): 169576 micro seconds
Throw (try): 64118 micro seconds
Error (try): 63125 micro seconds

Throwing an error or an exception take the same amount of time but using catch on an error is 2.5 times slower than using try … catch (my quick conclusion on that is because catch generates a stack trace).

I would like having only one way of catching exceptions (no catch, only try … catch) and a way to specify if I want a stack trace or not in case of error. Maybe something like this:

try Fun()
catch
  Error:Reason:Stack ->
    {Error:Reason:Stack}
end.

If a catch clause is waiting for 3 elements (Error, Reason, Stack), the compiler add the necessary stuff to call the Fun with a stack trace. If there are only 2 elements (Error, Reason), keep the actual behavior.

Do we really need exceptions in Erlang?

Exceptions may be useful but we can achieve the same goal in Erlang in other ways. Lots of functions have a signature like: {ok, Value} | {error, Reason}.

Those functions have different outputs between the normal case and the exceptional case. Combined with case we get the same behavior as catching an exception. If we don't use case, we get a badmatch error.

case_way() ->
  Fun = fun(ok) -> {ok, "It works"};
    (nok) -> {error, "It does not work"}
  end,
  run_case(1, Fun, ok),
  run_case(2, Fun, nok),
  ok.

run_case(ID, Fun, Arg) ->
  case Fun(Arg) of
    {error, Reason} ->
      io:fwrite("~p) Error: ~p~n", [ID, Reason]);
    {ok, Value} ->
      io:fwrite("~p) Value: ~p~n", [ID, Value])
  end.

Running case_way:

1) Value: "It works"
2) Error: "It does not work"

Boons:

  • Way much faster than exceptions (around 4 times faster according to my quick bench)
  • No specific syntax

Banes:

  • Normal case needs some encapsulation like {ok, Value} instead or returning Value directly (not doing so may lead to unknown states if the return is not pattern matched for errors)
  • No convention (you can return {exit, Type, Reason} if you want, or anything you want, developer needs to read carefully documentation of each function before using it)

In Joe Armstrong's book, he says that usually people use {error, Reason} when an error occurs quite often and exceptions for less frequent errors. It makes sense, but I do not completely agree with Joe on that matter. When you are developing a software for yourself (or you firm), you may know how your code is used so you know if an error occurs often or not. But when you are developing software for other developers, you don't know how they are going to use it, it's more difficult to "predict" if the error is going to be thrown often or not.

So I tend to prefer throwing exceptions (maybe I have used Java for too long). I think the added syntax is necessary in order to keep things clear, homogeneous and easy to use. I would love seeing a version of Erlang without functions having a {error, Reason} thing in their return signature (but something like throw(Reason)).

Complete source code associated with this post (you can do whatever you want with it): exceptions.erl.

Comments Add one by sending me an email.

  • From charpi ·

    I seldom use exceptions.

    Usually I prefer let the process crach with a badmatch if something wrong happen.

    I also distinguish errors and exceptions. For me, errors can let the caller do something in the error case if they want. I use exceptions when the problem is serious (file system full, network problem ....) and the only solution is to restart a process or the complete system

  • From Dominic Williams ·

    Hi Laurent,

    I don't understand your point about using erlang:get_stacktrace() from within the catch clause of a try/catch. As far as I know in theory, and have observed in practice, you get exactly the same stack trace as with a plain catch.

    Regarding the philosophy and style of error handling in Erlang, and the rationale for the new try/catch, I strongly recommend this excellent paper:

    http://www.erlang.se/workshop/2004/exception.pdf

    -- Dominic