Should we eventually improve catch&throw semantics?

I was persuaded to implement catch/trow in the first Erlang VM (the now obsolete JAM). I wish I hadn’t. Try/catch/end is much better and easier to understand. Don’t teach catch/throw, just tell people to avoid using them,

20 Likes

Ok, let’s agree to disagree then :wink: Just a few notes from me. Re-reading my previous posts, I realize that my statements were oversimplified/-generalized, so let me clarify :blush:

For one, I’m not against the use of throw in general. What I mean is that when used, it should be done in an “educated” way (“when used correctly” :wink:), as the specialized tool that it IMO is, not as a different general-purpose way of returning things from a function. Know what I mean?

As such, of course the documentation should mention and explain/describe it extensively. Omitting or glossing it over with “don’t use” would probably even create a counterproductive effect, as it makes the base knowledge for an educated decision when and when not to use it hard to attain. My emphasis is pretty much on educated use, ie “when used correctly” as you said, and the point that you and others already brought up “don’ts”, like “don’t throw out of your API”, illustrate that there is a wide field for uneducated misuse.

On beginners. I’m not saying that learning material for beginners should flag throw as something bad. IMO, they should not mention throw at all. They are usually still struggling with the basic everyday stuff and have to juggle a lot of it in their minds. I think throw is material for intermediate or advanced courses, where the students are comfortable with the basics, and you can go like “You know how to do X the regular way, right? But in circumstances like Y and Z there is a better way, throw, which I’ll explain to you now”.

As an analogy, think of a beginner course for cooking. You may talk about salt and pepper, ie what you are likely to use in basically any meal you will ever cook, and teach the students how to use them right. You probably won’t go into the more special stuff like curry and wasabi and whatnot, which you only need for special meals, that is material for later, specialized courses on Indian or Japanese cooking.
(Btw, I’m a horrible cook, so take this analogy with a grain of salt :stuck_out_tongue_winking_eye:)

Anyway. As @elbrujohalcon said, “that ship has sailed a while ago”. From what he said in the opening post (from the resources he mentioned later, I’m only familiar with LYSE), it looks like that throw creates confusion even when introduced relatively late (LYSE does in chapter 9 I think). Therefore I believe that when some code contains throws, chances are that it is used uneducated, and therefore the elvis rule is IMO justified to serve as a friendly “are you sure you want to do it that way?” (as opposed to “don’t use throw”).


Granted, that is one way to look at it :wink:

Anyway, this is how I think about it:

(3) = fun (...) -> ... end.
            ^       ^
            |       |
           (1)     (2)

In a nutshell, stuff goes in at (1) (the input arguments) and something is done with it at (2). In a “regular” (for want of a better word) function, stuff comes out at (3) (the output/result).

But with throw somewhere in the course of (2), nothing comes out at (3), instead the output pops into existence (figuratively speaking) somewhere else, at (4) (not in the picture :stuck_out_tongue:) or alternatively crashes the process if it goes uncatched.

Now, if it would still come out at (3) (like it would with a return as known from, say, Java), it would be a different matter, but as it turns up somewhere else, well… :smile:

7 Likes

… and hello Mike :wink:

4 Likes

I also think that return has a different purpose.
The non-local return nature of throw allows me to write a shorter and more readable code. Like this:

try
    outer(inner(Arg))
catch
    throw:{inner, reason} -> ...
    throw:{outer, reason} -> ...
end

I prefer this to “tupple-wrapping”

case inner(Arg) of
    {ok, InnerResult} ->
        case outer(InnerResult) of
            {ok, Success} ->
                Success;
            {error, reason, OriginalStack} -> 
                 ...
         end;
    {error, reason, AnotherStack} ->
        ...
end.

Added maybe syntax reduces the barrage of case ... of clauses. But I still think that outer(inner(Arg)) is easier to understand and reason about - “call the outer function with the result returned by inner”. maybe it is still not as concise. Not beautiful.

Having throw/catch allows me to write error handlers precisely where I need them. At the level where I can write a code that recovers from an error. Plus it saves the call stack - unlike “bubble wrapping” in {error, Reason} tuples where call stack needs to be added third like {error, Reason, OriginalErrorCallStack} - re-creating exception handling wheel.

The only feature I am missing is lack of type-checking support from Dialyzer.

5 Likes

Yes, I wont force you to agree with me :slightly_smiling_face:

:+1: I agree

Yes, it is important that it is well described how/when to use it. I don’t think it is a primitive that should be used everywhere. My main objection is against discouraging the use of it. If you do that, I guess that it will be more or less forbidden to be used in some projects since it is quite reasonable to forbid usage of constructs that official documentation is discouraging usage of. In my opinion the language as such would not be as good as it is today, the day we discourage use of throw. That is, we would end up with a worse language.

I haven’t used elvis, so I’m not sure what it is supposed to do or not. I’ll put it on the todo-list to take a look at. In general I think a tool shouldn’t splash too much “are you sure” in your face, though. It easily becomes more of an annoyance than a help (if so I stop using such tools), but as I said I haven’t used elvis so I don’t know what I think of this specific case.

I would actually say that it more or less is what it is and not just how I look at it.

Conceptually both try-throw-catch and a return takes you out of a scope with an associated value. Returning is more or less just a special case of try-throw-catch in that it only takes you out of the scope of current function. Return can more or less be viewed as syntactic sugar for a try function() catch throw:ReturnValue -> ReturnValue end where function() use throw(ReturnValue) for the return.

It is also very well defined how the thrown value gets out of the scope. You always end up in the surrounding scope. If there is a try-catch there, there is where you stop and return the value. It is also the same thing with the special case return.

So you are more or less always using try-throw-catch in every function you write :smiling_face_with_three_hearts:

To me the returned value at the caller “pops into existence” just as much as a caught thrown value “pops into existence”.

3 Likes

Hm, you do have a point, but doing a quick search, it looks like this ship has sailed as well :thinking: I guess it may even have been some faint reminiscences of this that lead me towards my initial reaction here.

Also, the Erlang Users Guide mentions throw only briefly, and in “Errors and Error Handling” at that. The manual on throw also talks about it as a way to raise exceptions (to be fair, it also says “Intended to be used to do non-local returns from functions” :wink:). And while this may not be wrong, it sort of explains the confusion that gave rise to this thread.

So I guess some rewriting and elaboration is in order.

TBH, same here :rofl: Style checker / code sniffer of sorts I’d say.

4 Likes

One of the use cases I haven’t seen pointed out here is that few people know that OTP as a framework has decided to support throw as a non-local return.

If you use a gen_server (or any other OTP behaviour), throws are supported as a way to return values:

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

Similar code exists for each handler, meaning that you are free, in a deeper recursive case, to do things such as:

handle_call(Msg, _From, State=#{...}) ->
    validate(Msg, State),
    NewState = do_handle(Msg, State),
    some_logging(Msg),
    {reply, ok, State};
...

validate(..., ...) -> ok;
validate(Args, State) -> throw({reply, {error, {invalid_arguments, Args}, State}}).

and have it work properly.

Most people discover that this is supported by accident when code unexpectedly throws a non-OTP value and they get a crash for an invalid state transition.

I don’t know if people would use it more if they knew it existed, or would be simply annoyed by the surprise.

9 Likes

Wasn’t this a side effect of the implementation being based on catch, which now remains for backwards compatibility?

I believe the more recent behaviors do not support it.

4 Likes

Yes, I think that is the reason.

3 Likes

I’m not sure whether it’s supported. Yes, it’s in the code. But I’m not sure whether it is a documented behaviour. I’m trying not to make any of my code depend on this detail.

4 Likes

That throwing a value from a callback is a valid way of returning a value is documented in gen_statem, so I see no reason it should not be documented in gen_server.

It is an ancient behaviour, that may have been created by accident, but for gen_statem there is at least one valid use case: to throw a postpone-and-stay-in-the-same-state return value from deep down in some common message processing function that thanks to this feature does not need to know the state nor the data - only the message.

5 Likes

Originally there was first throw/catch which was added as a way to handle non-local returns; you do a throw deep-down and it would be caught with a catch. That is why catch doesn’t wrap a thrown value and just returns the value, the caller should not see the difference. Catch errors/exits was also added and to show that that is what had happened the error/exit was wrapped in an {'EXIT', Error} tuple.

try/catch came later.

6 Likes