The Exceptional Server

Brujo Benavides
Erlang Battleground
4 min readAug 2, 2016

--

I discovered what I will show you today the first time I tried to create a gen_server based behaviour in Erlang. For wpool, I think. Anyway, I forgot about it for a long time until Francesco Cesarini reminded me of it at our last company Get2Gether.

This is the story of a very efficient server…

Catherine Zeta-Jones, Tom Hanks, Kumar Pallana — The Terminal (2004)

The Code

Let me introduce you to Jaime:

Jaime is a gen_server. It basically carries things in his tray and eventually delivers them all together. The tricky part is that, if he trips with something, he’ll throw the last thing he put on his tray. But he will do it so efficiently that if you want, you can totally grab that thing before it… you know… hits the floor or something.

Most of Jaime’s code is your regular gen_server’s code. The main part is in handle_call. Let’s check its three clauses one by one…

handle_call(deliver, _From, Tray) -> {reply, Tray, []};

The first one is a classic one: The server gets a deliver message and returns the whole state (in this case the Tray) and in the same process alters the state so that the new Tray becomes empty. Let’s give it a try on the console…

1> jaime:start().
{ok,<0.68.0>}
2> jaime:deliver().
[]
3> jaime:carry(coke).
ok
4> jaime:carry(pasta).
ok
5> jaime:deliver().
[pasta,coke]
6> jaime:deliver().
[]
7>

Everything looks quite right. Let’s check the next clause…

handle_call(trip, _From, []) -> throw(empty_tray);

This is an edge case. The requirements stated that, in case of tripping, Jaime should throw the most recent item on his Tray. What should Jaime do if there are no items on it is not specified. The proper thing to do would’ve been to just not add a clause for that, but let’s imagine that we wanted to provide a clearer exception in this situation and not just a function_clause. Let’s see what happens…

1> jaime:start().
{ok,<0.73.0>}
2> jaime:trip().
=ERROR REPORT==== 2-Aug-2016::08:17:58 ===
** Generic server jaime terminating
** Last message in was trip
** When Server state == []
** Reason for termination ==
** {bad_return_value,empty_tray}
** exception exit: {{bad_return_value,empty_tray},
{gen_server,call,[jaime,trip]}}
in function gen_server:call/2 (gen_server.erl, line 204)
3>

Ok, so… From the perspective of the caller, we get the empty_tray exception somewhere. Also the server crashes, which was expected. But there is one curious thing: We don’t simply get an empty_tray exception, don’t we? It’s actually a bad_return_value. Before diving into what this means, let’s look at the last function clause:

handle_call(trip, _From, [Stuff|Tray]) ->
throw({reply, Stuff, Tray}).

This one is weird. Jaime is actually throwing an exception. But the exception looks a lot like a proper handle_call response. Let’s see what happens if we try to use it…

1> jaime:start().
{ok,<0.68.0>}
2> jaime:carry(coke).
ok
3> jaime:carry(pasta).
ok
4> jaime:trip().
pasta
5> jaime:deliver().
[coke]
6>
Toro the Bull expected an exception

Wait a second! There are absolutely no traces of any exception there. The gen_server just acted as if the exception Jaime threw was a regular callback response.

Whats going on here?

One more time, I’ll recommend you to check Erlang/OTP docs and probably it’s code and try to figure out what happens on your own before reading any further.

Now, let’s do it together. First, let’s check what the docs have to say about this…

The closest thing I could find was:

If a callback function fails or returns a bad value, the gen_server process terminates.

That seems to be the case if we consider the empty_tray exception situation above. Although it doesn’t hold for our reply-like exception, does it? Actually, it doesn’t say anything about throwing exceptions.

We’ll have to dig into the OTP code to see what’s going on. This is the part of gen_server’s code where it’s evaluating handle_call callback:

try_handle_call(Mod, Msg, From, State) ->
try
{ok, Mod:handle_call(Msg, From, State)}
catch
throw:R ->
{ok, R};
error:R ->
Stacktrace = erlang:get_stacktrace(),
{'EXIT', {R, Stacktrace}, {R, Stacktrace}};
exit:R ->
Stacktrace = erlang:get_stacktrace(),
{'EXIT', R, {R, Stacktrace}}
end.

I highlighted the important section. It basically states that, if your module implements this behaviour and throws an exception from within your handle_call function, the exception itself will be treated as a regular reply. And the same happens for any other callback.

But, why? Well, the reason has a lot to do with backwards compatibility. You see, try was a somewhat recent addition to the Erlang language. By the time gen_server was written, erlangers managed exceptions using catch. As you can see in the manual page:

The BIF throw(Any) can be used for non-local return from a function. It must be evaluated within a catch, which returns the value Any.

Example:

5> catch throw(hello).
hello

If throw/1 is not evaluated within a catch, a nocatch run-time error occurs.

gen_server code, before Oct. 2014, looked like this:

case catch Mod:handle_call(Msg, From, State) of
{reply, Reply, NState} ->
reply(From, Reply),
loop(Parent, Name, NState, Mod, infinity, []);
{reply, Reply, NState, Time1} ->
reply(From, Reply),
loop(Parent, Name, NState, Mod, Time1, []);
{noreply, NState} ->
loop(Parent, Name, NState, Mod, infinity, []);
{noreply, NState, Time1} ->
loop(Parent, Name, NState, Mod, Time1, []);
{stop, Reason, Reply, NState} ->
{'EXIT', R} =
(catch terminate(Reason, Name, Msg, Mod, NState, [])),
reply(From, Reply),
exit(R);
Other ->
handle_common_reply(Other, Parent, Name, Msg, Mod, State)
end;

And that, combined with some techniques we’ve seen before, allows you to write really clever code like…

handle_call({send, User, Message}, _From, State) ->  is_valid(User) orelse
throw({reply, {error, invalid_user}, State}),
[ throw({reply, {error, invalid_message}, State})
|| is_well_formed(Message)],
{DeliveryCode, NewState} = send_message(User, Message, State), {reply, {ok, DeliveryCode}, NewState}.

Please don’t do this at home kids!

--

--