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 returningValue
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.
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
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