Error handling in Erlang

Last time, I wrote about how to write a hello world HTTP server by using Erlang, Cowboy and Rebar3.

Walkthrough of hello world HTTP server with Erlang, Cowboy, and Rebar3

Today, I'm going to write about the error handling in Erlang.

The error handling in Erlang is easy if we can ignore the error. That is, we don't expect the error. In the event of unlikely failure, we immediately abandon any computation we were doing and start over. The OTP and other famous frameworks were written by following Erlang culture of error handling so it will handle it well. If, on the ohter hand, the errors are expected to happen, so we have to explicitly deal with it. Erlang is suddenly reared its ugly head.

How a function notify it's caller the failure? In other languages, like Haskell, there is a Maybe type which may or may not contains the value. Erlang is not staticaly strong typed language so the Erlang version of the Maybe is tagged tuple. We return ok or {ok, State} upon success and we return any other value otherwise. It may be {error, Reason}, empty list [], or simply an atom like error, false, undefined whatever.

The user of such functions must match the returned value with ok tagged tuple,

do_something( State ) ->
    {ok, State_1} = f( State ),

If the function f return anything other than {ok, any()}, match failed and throw an exception of error:{badmatch, V}. So hopefully, the higher framework catch these exceptions and restart the process.

But what if the caller want to deal with the error? We have to use the patter match for the conditional execution. It can be done by case expression or function.

case expression:

do_something( State ) ->
    case f( State ) of
        { ok, State_1 } -> do_normal_things ;
        { error, Reason } -> do_error_handling

function :

do_something( State ) ->
    do_something_1( f( State ) ) .

do_something_1( { ok, State } ) -> do_normal_things ;
do_something_1( {error, Reason} ) -> do_error_handling .

Whichever you use, it became quite verbose and boilar-plate.

Erlang has exception like many other langauges. But I feel some oddities on Erlang's exception and that is the concept of class.

The Erlang has three class of exception: error, exit, and throw. These are thrown by calling function error/1,2, exit/1, and throw/1 respectively.

If you don't care about exception, you need to do nothing. But if you care, that is, you want to run some code on the condition of exception, things get verbose.

Let's suppose that previous function f/1 return { ok, State } on success, but throw some exceptions otherwise and you want to deal with it because you expect it to happen. You can use try expression or catch expression

try expression is strightforward, if you ignore the awkward Erlang grammer that is.

try Exprs
    catch Class1:Pattern1 -> Body1 ;
    catch Class2:Pattern2 -> Body2

The class is either error, exit, or throw, pattern may vary. If you were to catch the exception thrown by error class's badmatch(1 = 2), it looks like this.

try 1 = 2
    catch error:{ badmatch, V } -> its_bad_man

Now, how to do_normal_thing and do_error_handling depending on the presense of exception? try expression can have of section and it will be evaluated only on no exception in Exprs.

try f( State ) of
    { ok, State_1 } -> do_normal_thing ;
    throw:something_went_bad -> do_error_handling

Now how to deal with the situation where the error will be reported in either by value or exception? Use try expression's of section to pattern match the error value.

try f( State ) of
    { ok, State_1 } -> do_normal_thing ;
    { error, Reason } -> do_error_handling
    throw:something_went_bad -> do_error_handling

There is another way to deal with the exception. The catch expression.

catch Exprs

catch expression evaluate Exprs and return its value on no exception. In case of exception, the value will be either

For exceptions of class error, that is, run-time errors, {'EXIT',{Reason,Stack}} is returned.

For exceptions of class exit, that is, the code called exit(Term), {'EXIT',Term} is returned.

For exceptions of class throw, that is the code called throw(Term), Term is returned.

Erlang -- Expressions

If it's by throw({error, Reason}), the code will be clean.

case catch Exprs of
    { ok, Value } -> do_normal_thing ;
    { error, Reason } -> do_error_handling

But if it's error class, the code is too ulgy to read.

case catch 1 = 2 of
    { 'EXIT', { {badmatch, _} }, _ } -> do_error_handling

Perhaps, error and exit class were not meant to be handled by catch expression, but some library use these even for the predictable situations. like list_to_integer, binary_to_integer. My guess is to keep the backward compatibility.

Putting it all togather, it's very verbose to handle errors in Erlang.

Let's quickly borrow a code from the hello world HTTP server I explained in the previous article. Walkthrough of hello world HTTP server with Erlang, Cowboy, and Rebar3

Instead of returning the hello world, we're going to return the sum of two query parameter a, b.

$ curl "http://localhost:8080/?a=1&b=2"
$ curl "http://localhost:8080/?a=1&b=-100"

All we need to do is modify the cowboy_handler. The simplest code that assume no error will be like this.

init( Req, State ) ->
    P = cowboy_req:parse_qs(Req),
    { _, A } = lists:keyfind(<<"a">>, 1, P ),
    { _, B } = lists:keyfind(<<"b">>, 1, P ),
    Sum = integer_to_binary( binary_to_integer(A) + binary_to_integer(B) ),
    Req_1 = cowboy_req:reply( 200,
        #{<<"content-type">> => <<"text/plain">>},
        Sum, Req ),
    {ok, Req_1, State ).

Well, it's not bad. But I want to deal the the error.

Suppose, the users forget the query parameters.

$ curl "http://localhost:8080/"
$ curl "http://localhost:8080/?a=1"
$ curl "http://localhost:8080/?b=1"

If this happend, our code failed the pattern match because lists:keyfind returns false.

{ _, A } = false,

In such cases, I want to reply with the helpful error messages like this.

$ curl "http://localhost:8080/"
Error: missing query parameter a, b.
$ curl "http://localhost:8080/?a=1"
Error: missing query parameter b.
$ curl "http://localhost:8080/?b=1"
Error: missing query parameter a.

We can do condional branching with either case expression or function pattern match.

Another type of error is although the query paramters are present, it has a string that cannot be parsed as an integer.

$ curl "http://localhost:8080/?a=abc&b=123"

I would like to reply with helpful error messages in this case too.

After consdering the various code, I come up with this code. It's too verbose and ugly but I think alternatives are worse.

init( Req, State ) ->
    P = cowboy_req:parse_qs(Req),
    A = lists:keyfind( <<"a">>, 1, P ),
    B = lists:keyfind( <<"b">, 1, P ),
    { Http_status_code, Answer } = process( A, B ),

    Req_1 = cowboy_req:reply( Http_status_code,
        #{&lt;&lt;"content-type"&gt;&gt; =&gt; &lt;&lt;"text/plain"&gt;&gt;},
        Answer, Req ),
    { ok, Req_1, State }.

process/2 is set of function that ultimately returns { integer(), iodata() }. Here is the verbose code.

%% for missing query parameters.
process( false, false )     -> { 400, <<"Error: missing query parameter a, b.\n">> } ;
process( false, _ )         -> { 400, <<"Error: missing query parameter a.\n">> } ;
process( _, false )         -> { 400, <<"Error: missing query parameter b.\n">> } ;
%% for invalid query parameters
process( badarg, bardarg)   -> { 400, <<"Error: invalid query parameter a, b.\n">> } ;
process( badarg, _ )        -> { 400, <<"Error: invalid query parameter a.\n">> } ;
process( _, bardarg)        -> { 400, <<"Error: invalid query parameter b.\n">> } ;
% lists:keyfind succeeded.
process( { _, A }, { _, B } ) ->
        try binary_to_integer( A ) catch error:badarg -> badarg end,
        try binary_to_integer( B ) catch error:badarg -> badarg end
    ) ;
% no invalid query parameter. return the result.
process( A, B ) ->
    { 200, { integer_to_binary( A + B ), <<"\n">> } } .

The -spec attribute for this process/2 is abomination.

-spec process(
    { bitstring(), bitstring() } | false | badarg | integer(),
    { bitstring(), bitstring() } | false | badarg | integer()
)  -> { integer(), iodata() }.

Well, at least, I understand the error handling of Erlang.


Cardo said...

Nice article! keep writing this kind of blog. Keep it up, thanks budddy!

Edwin said...

I bookmarked more article from this website. Nice blog provided here.

Kane said...

Very nice article and straight to the point. Keep it up, Thanks

Saint said...

This website is useful. You made great points Many thanks for sharing.