[eeps] EEP ???: Value-Based Error Handling Mechanisms

Fred Hebert mononcqc@REDACTED
Tue Sep 11 17:05:18 CEST 2018


On Mon, Sep 10, 2018 at 10:48 AM, e@REDACTED <e@REDACTED> wrote:

> i'm sorry to interrupt,
> but all you REALLY NEED is to tweak the "try..catch" a little bit,
> in a manner that the catch clause receives and returns
> more comprehensible values, the values that are clearly linked
> with the failed expressions and human readable.
>
> you do not need a new messy CRYPTIC operator.
>
> try
>   {ok, Result} = foo(...)
> , {ok, _} = foo2(...)
> catch
>  {somehow_identify_which_line_failed, unobscured_the_rightside_value}
>

This here is exactly the problem -- you can't somehow identify which line
failed unobscured with the right value. First, you need to be able to
analyze the stacktrace for that, but there is no mechanism to do so. The
new stacktrace syntax specifies that it can't be a pattern match, and the
old form requires you to call 'get_stacktrace()' within the clause, which
prevents you from doing wide-matches (you'd need a case expression within
the catch), and then you'd need to re-throw. This also has a performance
cost, but let's assume we could get it for free (this needs new magical
syntax as well). Take for example:

succeeds_n_times(0) -> ok;
succeeds_n_times(N) when N > 0 ->
    try
        risky_operation(),
        succeeds_n_times(N-1)
    catch
        throw:risky_operation_failed:_Stack:{?MODULE, AcceptedLine} ->
            {error, risky_operation_failed}
    end

Note that this form breaks tail-recursion. A more proper one would look
like:

succeeds_n_times(0) -> ok;
succeeds_n_times(N) when N > 0 ->
    try risky_operation() of  % we need 'of' to preserve tail-recursion
        ok -> succeeds_n_times(N-1)
    catch
        throw:risky_operation_failed:_Stacktrace:{?MODULE, AcceptedLine} ->
            {error, risky_operation_failed}
    end

And now it would work. The problem of course is that we don't have an
easily spotted exception there. What we can work with if we don't want to
annotate all of the calls to risky code is only a badmatch:

succeeds_n_times(0) -> ok;
succeeds_n_times(N) when N > 0 ->
    try risky_operation() of  % we need 'of' to preserve tail-recursion
        ok -> succeeds_n_times(N-1)
    catch
        error:{badmatch, _}:_Stacktrace:{?MODULE, AcceptedLine} ->
            {error, risky_operation_failed}
    end

so this could work, but only as long as you always have the right module
and the right line number. If you don't match this correctly, you may catch
failures from other nested calls. The downside is obviously that you don't
want to tie your patterns to line numbers, since any change in unrelated
code may break all of your error handling. But without line numbers there
is no good way to know, from a remote scope, whether you wanted something
to match-or-return, or to match-and-abort.

Even if we had a syntax for the function name as well:

succeeds_n_times(0) -> ok;
succeeds_n_times(N) when N > 0 ->
    try
        ok = risky_operation1(),
        {ok, _} = risky_operation2()
    of
        ok -> succeeds_n_times(N-1)
    catch
        error:{badmatch, _}:_Stacktrace:{?MODULE, {succeeds_n_times, 1},
_Line} ->
            {error, risky_operation_failed}
    end

Then you cannot know whether you're capturing risky_operation1 or
risky_operation2 without specifying and accounting for all line numbers.
This is impractical and brittle. The only way to do this kind of flow
safely is with throws identifying every call-site and then we're back to
exception-based error handling. Compare with the proposed format:

succeeds_n_times(0) -> ok;
succeeds_n_times(N) when N > 0 ->
    begin
        _ <~ risky_operation1(), % captured
        {ok, _} = risky_operation2(), % not captured; hard assertion
        succeeds_n_times(N-1)
    end

 This form only captures what it has to capture, and does not have any
pitfalls hidden regarding tail recursion.


> and if (i am ignorant about that) "try" has drawbacks,
> then reimplement it the way you intend to implement your new operator.
>
>
That's the thing; there is no way to get a semantically equivalent
mechanism with try ... catch without instrumenting the called code rather
than just the callers. The easiest semantic comparison is with nested case
... of expressions, not try ... catch. Even if we extended try ... catch to
somehow be able to only capture errors in its direct scope, we couldn't
easily distinguish between calls we want to fail on a badmatch and those we
won't. Line numbers are poor identifiers since they're very easy to change.
A line of comments or a re-wrapping of exports could break all error
handling in a module.

On Mon, Sep 10, 2018 at 10:48 AM, e@REDACTED <e@REDACTED> wrote:

> i'm sorry to interrupt,
> but all you REALLY NEED is to tweak the "try..catch" a little bit,
> in a manner that the catch clause receives and returns
> more comprehensible values, the values that are clearly linked
> with the failed expressions and human readable.
>
> you do not need a new messy CRYPTIC operator.
>
> try
>   {ok, Result} = foo(...)
> , {ok, _} = foo2(...)
> catch
>  {somehow_identify_which_line_failed, unobscured_the_rightside_value}
>     ->  ...
>   -
>     -> default_returnvalue
> end
>
> and if (i am ignorant about that) "try" has drawbacks,
> then reimplement it the way you intend to implement your new operator.
>
>
>
> _______________________________________________
> eeps mailing list
> eeps@REDACTED
> http://erlang.org/mailman/listinfo/eeps
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://erlang.org/pipermail/eeps/attachments/20180911/2d7fefdc/attachment.htm>


More information about the eeps mailing list