Philip Kamenarsky
Philip Kamenarsky

Reputation: 2777

Erlang error handling philosophy - case vs throw

I'm writing a REST service in Erlang and need to verify the received data before passing it to other internal functions for further processing; in order to do that, I'm currently using nested case expressions like this:

case all_args_defined(Args) of
    true ->
        ActionSuccess = action(Args),

        case ActionSuccess of
            {ok, _} -> ...;
            {fail, reason} -> {fail, reason}
        end,
    _ ->
        {fail, "args not defined"}
end,
...

I realize this is kind of ugly, but this way I can provide detailed error messages. Additionally, I don't think the usual make it crash philosophy is applicable here - I don't want my REST service to crash and be restarted every time somebody throws invalid arguments at it.

However, I'm considering abandoning all those cases in favor of an umbrella try/catch block catching any badmatch errors - would this work?

fun() ->
    true = all_args_defined(Args),
    {ok, _} = action(Args).

%% somewhere else
catch fun().

Upvotes: 6

Views: 3377

Answers (3)

David Weldon
David Weldon

Reputation: 64312

I have faced exactly the same question when writing my own REST services.

Let's start with the philosophy:

I like to think of my applications like a box. On the inside of the box are all of the parts I built and have direct control over. If something breaks here, it's my fault, it should crash, and I should read about it in an error log. On the edge of the box are all of the connection points to the outside world - these are not to be trusted. I avoid exception handling in the inside parts and use it as needed for the outer edge.

On similar projects I have worked on:

I usually have about a dozen checks on the user input. If something looks bad, I log it and return an error to the user. Having a stack trace isn't particularly meaningful to me - if the user forgot a parameter there is nothing in my code to hunt down and fix. I'd rather see a text log that says something like: “at 17:35, user X accessed path Y but was missing parameter Z”.

I organize my checks into functions that return ok or {error, string()}. The main function just iterates over the checks and returns ok if they all pass, otherwise it returns the first error, which is then logged. Inside of my check functions I use exception handling as needed because I can't possibly consider all of the ways users can screw up.

As suggested by my colleagues, you can alternatively have each check throw an exception instead of using a tuple.

As for your implementation, I think your idea of using a single exception handler is a good one if you only have the single check. If you end up needing more checks you may want to implement something like I described so that you can have more specific logging.

Upvotes: 2

Isac
Isac

Reputation: 2068

I've had this problem while developing an application that create users.

I first come with a solution like this:

insert() ->
    try
        check_1(), % the check functions throw an exception on error.
        check_2(),
        check_3(),
        do_insert()
    catch
        throw:Error1 ->
            handle_error_1();
        throw:Error2 ->
            handle_error_2();
        _:Error ->
            internal_error()
    end.

The problem with this solution is that you lose the stack trace with the try...catch block. Instead of this, a better solution is:

insert() ->
    case catch execute() of
        ok -> all_ok;
        {FuncName, Error} ->
            handle_error(FuncName, Error);
        {'EXIT', Error} ->
            internal_error(Error)
    end.

execute() ->
    check_1(), % the check functions throw an exception on error.
    check_2(),
    check_3(),
    do_insert().

This way you have the full error stack on Error.

Upvotes: 2

RichardC
RichardC

Reputation: 10557

Since what you want to achieve is error reporting, you should structure the thing around the execution of actions and reporting of the result. Perhaps something like this:


  execute(Action, Args) ->
    try
      check_args(Args),
      Result = action(Action, Args),
      send_result(Result)
    catch
      throw:{fail, Reason} ->
        report_error(Reason);
      ExceptionClass:Term ->
        %% catch-all for all other unexpected exceptions
        Trace = erlang:get_stacktrace(),
        report_error({crash, ExceptionClass, Term, Trace})
    end.

  %% all of these throw {fail, Reason} if they detect something fishy
  %% and otherwise they return some value as result (or just crash)
  action(foo, [X1, X2]) -> ...;
  action(foo, Args) -> throw({fail, {bad_arity, foo, 2, Args}});
  action(...) -> ...

  %% this handles the formatting of all possible errors 
  report_error({bad_arity, Action, Arity, Args}) ->
    send_error(io_lib:format("wrong number of arguments for ~w: "
                             "expected ~w, but got ~w",
                             [Action, Arity, length(Args)]));
  report_error(...) -> ...;
  report_error({crash, Class, Term, Trace}) ->
    send_error(io_lib:format("internal error: "
                             "~w:~w~nstacktrace:~n~p~n",
                             [Class, Term, Trace])).

Upvotes: 6

Related Questions