Should we eventually improve catch&throw semantics?

Reading @MRonErlang’s #book, and watching people struggle once more trying to explain catch and throw in Erlang… I came up with an idea :bulb:

Should we eventually true-up catch&throw?

The Problem

catch and its friend throw, were created to implement non-local return. That is, returning from a function before it’s done with its evaluation.
@MononcQc has probably the best explanation of it in LYSE, but the fact that he (and all of us trainers) needs to spend so much time teaching something that should be far simpler, it’s a symptom of the issue in general.

The Root Cause

I personally believe that, while throws, errors and exits do have some things in common, conflating them into a single semantic category never really benefited any Erlang programmer. And it was surely detrimental to those (like myself) who came from other languages where throw was clearly used to throw exceptions, not possible return values. For that, I expected to find something along the lines of return (or ^, in Smalltalk style).

The Proposed Solution

The Brutal Version

  • Rename throw as return.
  • Rename catch as handle_return (or something cleverer).
  • Stop handling errors with handle_return. The idea would be that handle_return will only handle… well… values returned from return.
  • Remove the ability to handle throws from try…catch…after…end. Or, alternatively, handle them as results, not as exceptions.

The More Amicable Version

  • Add a new keyword return that works exactly like throw.
  • Add a new keyword handle_return (or something cleverer) that works like catch but it doesn’t turn errors or exits into tuples.
  • Do not adjust try…catch…after…end to handle these new returns within the catch part.
    • If we feel like we need to handle them somehow, make handle_return implicit in try (i.e. as if every try Expr… would be try handle_return Expr, thus returning the results as values).
  • Slowly deprecate catch and throw over several OTP versions, recommending their replacement with the new keywords.
    • Or just deprecate catch making it evident that throw does throw actual exceptions that can only be captured within a try…catch… block.
6 Likes

I see there is a problem, but I have some concerns about your proposed solution.

The throw keyword is indeed present in other languages, where the functionality may be different. However the return keyword is also a very common keyword.
My concern is that introducing such a common keyword, but with a functionality that differs from many other languages will cause more confusion, especially for people new to the language.

Your proposed return and handle_return indeed make it clear that it is not limited to exceptions, but is in fact a non-local return.

However, I fear that this could lead people who are new to the language to believe that these keyword should always be used to define the control flow, because this is similar to how the keywords are used in other languages.

7 Likes

Yeah! You’re right. We need a different name. My proposed return is more similar to Smalltalk’s ^ than to an actual return keyword from other languages.

Maybe we need to invent a brand new keyword/concept for this…

:thinking:

3 Likes

I don’t think it should be made easier (or friendlier or whatever). Non-local returns are something that should not be done, not without some very good reasons, and even then, think twice.

My concern is that renaming or new-naming throw will make non-local returns look more acceptable.

The somewhat scary-sounding throw discourages usage, while a nice-sounding return (or whatever) encourages it. In other languages, you don’t throw things (exceptions usually) around light-heartedly, but you return stuff all the time.

So, IMO, leave well alone. If it’s difficult to explain or drive the meaning across, that’s a good thing. Things you don’t want to be done should not be made easy to do.

10 Likes

Do you mind sharing a bit more about the reasoning behind your idea that non-local returns should not be used, in general?
I think it’s the first time I read something like that.
Should we create an Elvis rule to avoid the use of throw?
I mean… It should not be used for exceptions (that’s what error and exit are for). So, if it should also not be used for non-local returns… we can just simply emit a warning if it’s used. :thinking:
Anyway… I’m interested in learning more about the reasons to avoid non-local returns.

2 Likes

On the other hand, at least in my experience, the name throw may discourage its use for non-local returns, but at the same time, it encourages its use for exceptions. Which is the main problem I usually face: People trying to throw exceptions using throw as they do in Java, C#, etc., instead of using error, raise, or exit (which are the ones meant to be used for that in Erlang).

4 Likes

IMO, it is really bad style, especially in the context of a functional language. It is returning something through side effects. That may have its uses to quickly break out of deep recursion, but they are rare. I think I never used throw even once. In any case, I think it should not be made “socially acceptable” to return stuff via throw, much less encouraged. But this is what return would suggest to me. I want to return something from my function, so I use return, what else could it be there for? :sweat_smile:

7 Likes

I definitely agree. I prefer to use tail-recursive functions instead of non-local returns unless that turns to be too cumbersome. That is, if the code becomes convoluted written in a tail-recursive way, I will write it in a body-recursive way and use throw to break out of the recursion. I have typically used that in complicated optimization passes in the compiler; that is, the code will do the optimization in recursive functions, but if anything turns up that makes it the optimization impossible I will throw a not_possible exception to break out of the recursion.

No, I don’t think there should be an Elvis rule to warn, because Elvis can’t possibly distinguish between necessary and unnecessary use of throw.

7 Likes

I agree that that is unfortunate, but I think it’s too late to change that now.

4 Likes

Yeah, exactly what @Soenderby pointed out before. The name is wrong because it will encourage devs to use it incorrectly, i.e. the same issue that throw has today.

3 Likes

Well… that’s fine. That happens with almost every rule in Elvis. Elvis doesn’t have dialyzer-style accuracy. Sometimes it warns you about code where you actually meant to bend or break a certain rule. That’s why you can use -elvis to remove those warnings.
If the use of throw is a bad smell, having a rule to detect that would be a good idea, IMHO.

4 Likes

If I were teaching Erlang, I wouldn’t even mention the existence of throw, at least not early on. It has the potential to drag out a bond with and carry over a practice from their old language (Java, C#, whatever), where throwing and catching exceptions is the usual way of error handling, with a main thread being kept alive at all costs. But the preferred way of error “handling” in Erlang is to just let it crash, no? :wink:

5 Likes

Well… That ship already sailed a while back, I’m afraid…
catch, throw, and try…catch…after…end are already present in the main books about Erlang, like LYSE, Programming Erlang, etc… even “Erlang/OTP: Un Mundo Concurrente” (which is the one that led me to start this thread).
It’s also present in online courses, like the one that @simonthompson delivers on FutureLearn… and several others.

But… Now that you opened my mind to a world without throws… I, at least, wrote a PR to add a new guideline and another one to code a new rule. It’s not much, but we need to start somewhere, right?

5 Likes

Add a new keyword return that works exactly like throw.

That sounds like goto, with handle_return serving a label role (although more dynamic for it can match on the Reason). It however matches the ERTS implementation.

Internally we have guidelines to use throw for non-local returns within a single module (within public API of a module), error when the exception is handled somewhere else, and exit when the caller left garbage in the process (e.g. destroyed process dictionary, or process state inconsistent, or something else that the process is no longer safe to swallow the error and continue running).

5 Likes

It isn’t a bad smell necessarily, it’s a highly specialized tool that is sometimes needed (but not as often as one might think).

5 Likes

Agreed. If we are speaking about proposals, I would:

  1. Add a note to the throw docs saying it should be discouraged

  2. Deprecate the use of try/catch without the specifier. Instead of:

    try TryExpr catch Pattern -> CatchExpr end.
    

    I would always ask users to write:

    try TryExpr catch throw:Pattern -> CatchExpr end.
    

    The reason is because it feels try/catch prioritizes throws, but in my experience it is the least of used of all 3 (throw/error/exit).

  3. Bonus: double discourage the use of catch Expr :smiley:

12 Likes

I like this proposal a lot. Even though I know how the categories work, I have run into this, especially when trying to catch “all”.

3 Likes

I don’t see an exception as a fault, as I guess is how you see it, but rather the action of unwinding the stack which may or may not be stopped by catch. How you look at what an exception is most likely depends on what language you come from. I took a look at what wikipedia had to say on exceptions and found this:

I guess Erlang would fall into the first category, and that you want to move it into the second category. I haven’t looked at all of these languages and it is a wikipedia page, but I think you get the point.

All of the different exception classes have the unwinding of the stack in common, and I see it as very natural that the try-catch can catch all of the different exception classes. The old style catch which cannot distinguish between the exception classes is/was horrible since it lumped them all together, but the “new” (somewhere around R7B) try-catch construct has solved this beautiful, in my opinion, by making it possible to specify exception class to catch. Introducing a handle_return would in my opinion not make it easier to understand the code, but instead harder.

I also do not see why the use of throw should be discouraged. I think bad usage of throw should be discouraged, but good use of it should be encouraged. You can write “hard to understand” code with it, but you can do so with most constructs. If you use it correctly, the code will be easier to understand and as icing on the cake, in some cases, more efficient by avoiding testing various conditions while returning up the call stack.

I agree that it should only be used inside the implementation of an API, and that you shouldn’t throw past the API functions.

I don’t see why it should be considered “not functional”. It does not have any side effects, only control flow effects.

Unfortunately Erlang lacks a return expression (working as it usually does, i.e., returning from current function) which would be extremely nice in order to avoid deeply nested cases. Deeply nested cases can become extremely hard to understand and often make “goto programing” look beautiful and very easy to understand in comparison. This since you often get code handling the return from one function call scattered all over the function. I also do not see why the use of case should be discouraged. I think bad usage of case should be discouraged, but good use of it should be encouraged. On some occasions I have resorted to using a try-throw-catch locally in a function in order to make such functions more readable. However, when returning locally, a return expression would have been much nicer.

I think the maybe construct will help a long way regarding deep case nesting although I don’t think it will help all use cases where you would want a return expression. I have not used the maybe construct yet, so maybe I’ll change my opinion when I have.

9 Likes

I somewhat disagree :sweat:

By encouraging good and discouraging bad usage, you have to specify what is good usage and what is bad usage. But you’re bound to leave a wide field open for interpretation and speculation, and we’re talking about people new to Erlang (if I read the opening post correctly), and they won’t know good from bad (yet) if it is not on the “good” or “bad” list.

IMO, it is better to make “Don’t use throw" a rule of thumb, and then enumerate the few exceptions where it is beneficial (”… unless (you think) you know what you’re doing").
In my experience, the use cases for an intended good use of throw are precious few, but the possibilities for unintended bad usage are numerous. For this reason, I think that it is justified to be initially suspicious when seeing throw used in code.

That may be so, but it is not an argument :smile_cat:

Mind you, I didn’t say “not functional”, I said “bad style” :wink:

That said, our definitions of “side effect” may differ a little here. My rule-of-thumb definition is “if something that happens inside the function causes something to happen or change outside of the function, it is a side effect”. By throwing, the output comes out “sideways”, not on the output end of the function where it is supposed to come out (and it needs to be explicitly catched).
Yes, the same could be said for errors and exits (explicit or implicit), but their use is limited to indicating that something happened that was not supposed to happen.

5 Likes

Yes, which more or less is what I’m getting at.

My opinion is that you want to describe how to and how not to use all different constructs, tools, etc, and not make a list of bad and good constructs. There will of course be things that should be put on a bad list, but such things should in my opinion have no good use cases and try-throw-catch definitely do not belong on such a list.

I don’t say that our documentation is perfect and describes how to and how not to do for all different constructs, etc, but I think it should. It is slowly but steadily improving. Take a look at the R6B documentation :yum:

When it comes to beginners. Course material and a like preferably order topics in an order suitable for beginners. This should perhaps not be in the first chapter, but I don’t think the correct way is to list something as bad just because it can be misused/misinterpreted by beginners.

Well, it is a counter argument against the (perhaps not literally expressed in this thread, but I think implied) argument that “you can write hard to understand/bad code with it”. As a counter argument to that I think it is a very good argument.

I interpreted that as being “not functional”, and I disagree with it. When used correctly I think it is the opposite of bad style.

Yes we have different definition of side effect and I don’t see it as coming out “sideways”, It is more or less a return which can return immediately from multiple functions and the catch is just the construct that stops this returning.

2 Likes