[erlang-questions] Try-catch-after considered harmful.

Fred Hebert mononcqc@REDACTED
Mon Nov 24 16:23:54 CET 2014


On 11/24, Joe Armstrong wrote:
> My first response is "don't use use try-catch-otherwise at all" let
> the process crash and let some other process fix up any damaging
> consequences of the crash.
> 
> The best was to write error handling code is to write no error
> handling code at all.
> 

You can get away with not handling errors when youcan afford not to
handle them, and that restarting is alright, and losing all the state is
alright.

There are cases where you have a subsystem that you know will fail or
timeoutand you want to handle that fine without tearing down everything
you have. You get on this idea of resources soon enough:

> At a first approximation, all the libraries are designed to be used
> this way.  If you open a resource like a file/socket/ets table/dets
> table/database table then if your program crashes the resource (which
> is represented by a process) will detect the crash and is supposed to
> "do the right thing" and clean up behind you.
> 

Yes, but what if what you want to do is display a thing such a 'parsing
event #328492 failed' rather than going silent all of a sudden?

It is the kind of code you cannot just keep retrying if the message is
badly formatted, and where letting the parser crash may make more sense
than just having it return `{error, badparse}` or whatever all the time.
Or maybe that's just not in your control.

So to work around that without a try ... catch, you need to fix it
through architecture:

- You spawn the parser in another process, either linked or monitored
- You go into a 'receive ... end' expression that listens to that single
  process you spawned
- If you receive the right message, it worked, if you received a
  monitor, it failed.

Moreover, if you want to know if it was an error, an exit, or a throw,
you have to go parse the `Reason' part of the message.

You'll notice that what we designed there is pretty much the exact same
thing as a try ... catch. Except we had to do it ad-hoc with processes,
links, and monitors.

Now let's try this mental exercise. I'm going to write a special syntax
for that operation:

try <Expr> catch <Patterns> after <Expr2> end.

This is going to be translated to:

    Parent = self(),
    {Pid, Ref} = spawn_monitor(fun() -> Parent ! {ok, Expr()} end),
    receive
        {ok, ExprRes} ->
            erlang:demonitor(Ref, [flush]),
            ExprRes;
        {'DOWN', Ref, process, Pid, Info} ->
            Patterns(Info)
    end,
    After()

This will give me roughly similar semantics to try ... catch, but it
will use processes instead. Now to keep it fully working in most cases,
I'll need to also make sure that:

 - File handlers are handed off (and back in)
 - ETS table descriptors are handed off (and back in)
 - Sockets are handed off (and back in)
 - Process dictionaries are sync'd
 - Tracing goes for more than one process
 - Etc.

The argument on whether you *need* anything but crash-fast could be had,
but with the assumption that it is *needed* today, try ... catch is
necessary if at the very least to keep people from having to write the
complex blob above.

I won't disagree with the idea that if you can, crash and restart. But
there are times when you can't, or where it's more confusing to do so.

> The try-catch syntax was deliberately chosen to be reminiscent of
> Java, the idea being that if you understood this try-catch consequence
> in Java you'd easily understand the Erlang code.
> 
> The problem I see with this is that programmers with previous
> experience in Java are tempted to blindly convert sequential try-catch
> code in Java into sequential try-catch code in Erlang, but this is
> almost always the wrong thing to do.
> 
> Beginners should be forbidden to use try-catch - the Erlang "way" is
> to spawn_link a regular process (ie a process that does not trap
> exits) and just let that process die if anything goes wrong. There
> should be no error trapping code in the spawned process. Call
> exit(Why) in every place where the behaviour is unspecified.
> 

try ... catch ... end is used for more than just exits.

- throw(X), which lets you do a non-local return if you need it to,
  without the need to spawn new processes, copy and synchronize the
  state, and then send a message back;
- error(X), programmer error in code
- exit(X), process can't keep going.

You do gain some richer semantics when using these 3 classes of
exceptions. Only when they are not caught or handled do they lead to the
bubbling of the exception into an exit(X), killing the process.

> The process doing the spawn_link should trap exits and report errors
> in the child processes. This is what all the supervisor and
> gen_my_aunties_hat stuff does in the standard libraries.
> 

the gen_stuff also runs try ... catches for you:

https://github.com/erlang/otp/blob/maint/lib/stdlib/src/gen_server.erl#L570-L631

Specifically, they handle the non-local returning, and transform the
exceptions into a value in order to let the behaviour take over and call
'terminate/N' on your behalf:

https://github.com/erlang/otp/blob/maint/lib/stdlib/src/gen_server.erl#L705-L720

They don't purely run links and monitors and receives. They have to take
over some of the program control flow to provide the mechanisms they
currently provide without needlessly messaging and carrying resource
across process boundaries all the time.

> If you really-really-really understand what changing the flow of
> execution in sequential code does then by all means try-catch-after -
> but the best advice is not to use it at all.
> 
> There should really be a compiler warning -
> 
> ** try-catch-after used - this is probably not the right way to do things
> 
> Cheers
> 

You'd get a warning in every single OTP module.

I get that try ... catch distracts from let-it-crash semantics, but it
doesn't mean that there is one true way to handle errors.

You can, quickly, count some ideas:

- Explicit checks for error values ({error, _}, {ok, _}) with code
  handling both cases
- Implicit assertions on error values with crashes on errors ({ok, _} = Expr)
- Monadic flow (Maybe/Either) for exception handling. You run the
  equivalent of this one in Erlang when every single branch of your
  code explicitly checks for error values
- Exceptions (throw/error + catching)
- Monitors
- Links

What try ... catch lets you do, for example, is change the decision
someone made to go with exceptions or assertions, and turn it into
whatever else you want -- You can transform an exception or a throw
into an exit signal, or transform a local exit signal into a value
in order to do your monadic flow or explicit checks.

What try ... catch gives you is not just Java mimicking, but a mechanism
to take control and unify whatever type of error handling your
dependencies use into the one your program needs.

Of course you should be careful to not use exceptions as your only
control flow mechanism for errors, but that's because others might be
the better tool. It's just not always the case.

Regards,
Fred.



More information about the erlang-questions mailing list