3
$\begingroup$

(I was not aware of this site and asked, by mistake, a similar question on cs.stackexchange as well. It is related to this question on this site.)

Context

I'm working on a new general-purpose programming language. Exception handling is supposed to be like in Rust, but simpler. I'm specially interested in feedback for exception handling (throw, catch).

I think, same as in Swift and Rust (and Go), exception should be handled immediately, and if not, either the program must stop, or the function must throw an exception. So, no undeclared exceptions. This is what "catch is needed, or the method needs throws" means.

Question

  • The catch catches all exceptions that were thrown within the scope. I argue there is no need for try, because try would requires (unnecessary, in my view) indentation, and messes up diffs. In Ruby, the equivalent of try is optional as well. Are there other languages like that? I didn't find any. Is there a problem with this idea?

Source Code Example

throw throws an exception. catch is needed, or the method needs throws:

fun factorial(x int) int throws
    if x > 20
        throw exception('Value too large')
    if x <= 1
        return 1
    f := factorial(x - 1)
  return x * f

fun main()
    i := 0
    while i <= 30
        println('Factorial of ' i ' is ' factorial(i))
        catch e
            println('Factorial of ' i ' resulted in ' e.message)
        i += 1
$\endgroup$
11
  • 1
    $\begingroup$ You might find this article about the error handling from the Midori project interesting. For instance, it allowed a definition of int Foo() throws { ... }, and a call-site sugar of Result<int> value = try Foo() else catch; $\endgroup$
    – IMSoP
    Commented Jun 27 at 15:07
  • 3
    $\begingroup$ What is a scope in your language? A function? A statement? Something else? What happens when catch is in the middle of a block - does its scope span the entire block, including after it? Does your language have the means to explicitly create a new scope? $\endgroup$ Commented Jun 27 at 15:24
  • 1
    $\begingroup$ @ThomasMueller As I said, you have the indentation for the if block, and then the catch block is indented further than that. If you had try instead of if then the catch block would be at the same level of indentation, instead of nested. As for which is the "common" case, your way is only correct if a try statement would be the first statement in its block, which in my experience is not that common. $\endgroup$
    – kaya3
    Commented Jun 27 at 17:45
  • 1
    $\begingroup$ Various Git clients have options to ignore whitespace, so indentation changing isn't that much of a problem $\endgroup$
    – Glorfindel
    Commented Jun 27 at 19:22
  • 2
    $\begingroup$ Provided an answer as there seems not to be a consensus to close and cleanup the question. I'm still in favor of removing the whitespace part as it is meaningless syntax and splitting the throws / nothrow part into a separate question. $\endgroup$
    – feldentm
    Commented Jun 29 at 8:49

5 Answers 5

7
$\begingroup$

If try is technically just a scope marker, it is functionally redundant when scopes are already clear. It is no problem to define a language in which try can be "folded" directly into a preceding scope marker.

For example, it is trivial to treat

while ...
  ...
catch e
  ...

as syntactic sugar for

while ...
  try
    ...
  catch e
    ...

which is similar to Ruby's semantics.

Relatedly, if your language is expression based you can have an <expr> catch <name> <expr(name)> construct/operator/... that trivially expands to a longer form. For example, this even was proposed for Python and only rejected for non-technical reasons.


What you should ask yourself is if exception handling for an entire, given scope is actually a good thing. Consider that your tiny example is intended to catch an error from factorial, but actually it would handle two to three things.

To clarify, let me write that part the way I would do for my dayjob:

try:
   result = factorial(i)
except e:
   print('Factorial of', i, 'resulted in', e.message)
   break
else:
   print('Factorial of', i, 'is', result)
   i += 1

Notice how the factorial handler really only deals with the factorial execution! It does not handle whether formatting strings works, whether printing works, or even whether incrementing works. These are all actions where an error is not expected by your handler, so if one does occur it should absolutely be loud and definitely not accidentally silenced.
Precisely handling errors is something you absolutely do want to offer. And probably (since correct error handling is your goal) you don't just want to offer it, but also encourage it - or even discourage imprecise error handling!

By inviting people to be sloppy and re-use logical scopes as error scopes, you make error handling worse. So at the very least, you should offer something to add explicit scopes. This can be a generic scope mechanism, and in your example syntax it would map to a separate keyword:

while ...
  .
  .
  scope
    .
  catch e
    ...

But at this point you are almost all the way to having try, especially if the scope has little or no other use. So basically you have to ask yourself whether you are willing to pay the price of having a try keyword or not - because most if not all of the other cost you should be prepared to pay anyway.

$\endgroup$
12
  • $\begingroup$ Thanks for the Python link! I think leaving out the "try" is not "sloppy", but a conscious decision to not have something. I understand removing things may not be popular for some people, that's fine. Btw. if true can be used instead of scope. $\endgroup$ Commented Jun 27 at 18:21
  • 1
    $\begingroup$ @ThomasMueller It’s not leaving out the try that is sloppy. It’s covering a far larger block than needed that is sloppy. $\endgroup$ Commented Jun 27 at 18:22
  • 1
    $\begingroup$ @ThomasMueller The harm, as explained in the answer for example, is that the handler will unintentionally catch errors it wasn’t supposed to catch. Especially for suppressing handlers, this means that veritable bugs are silenced. As for people having a problem with too broad exception handling, consider that for example the Python style guide recommends both small try blocks and precise handlers exactly for the reason I mentioned here. $\endgroup$ Commented Jun 27 at 18:44
  • 1
    $\begingroup$ @ThomasMueller Sure you can recommend people use if true to force scopes - but this then really only saves you not reserving the try/scope keyword. Is that minor saving really worth having people write less intuitive code? $\endgroup$ Commented Jun 27 at 19:01
  • 1
    $\begingroup$ @ThomasMueller If you have a scope that’s fifty lines long, but only two of those lines are expected to possibly throw an exception outside of cases you truly can’t recover from (for example, memory errors), is it better to cover just those two lines that you expect to possibly throw an exception, or the whole block? Any sane programmer is going to prefer to cover just those two lines, because any other line in that block throwing an exception should stop the program, because it’s either a bug or some other major issue. $\endgroup$ Commented Jun 27 at 20:44
5
$\begingroup$

The lack of an explicit try makes it harder to catch exceptions from only some places.

Put together with your other proposal to make exceptions homogeneous, it will be harder in multiple ways for programmers to catch and handle some exceptional cases while allowing others to propagate. It will also be more likely that programmers will mistakenly catch (and accidentally suppress) exceptions they did not intend to.


Consider code like this:

file = acquire_foo()

try:
    foo.bar()
    foo.baz()
catch:
    print('failed to bar and baz the foo')

This shape of code is very common, where we want to acquire a resource and do something with it, and handle the error if the latter fails. Note that the try: specifically occurs after acquire_foo(), meaning if we fail to acquire the resource, the exception will not be caught. This is the programmer's intention, but expressing that intention requires an explicit try. There is no easy way to write this code otherwise.

You could get around this by making catch only apply to the preceding statement, and allowing grouping using optional braces:

foo = acquire_foo()

{ // these together are one statement
    foo.bar()
    foo.baz()
}
catch:
    // this only catches exceptions from the previous block statement
    ...

However, besides having the same indentation you were trying to avoid by omitting try, this is an accident waiting to happen. People will forget to do this. It's particularly a problem when acquire_foo() and foo.bar() can signal the same type of exception (e.g. a database error), because if you catch from both by accident, you'll mishandle acquire_foo()'s error as if it was thrown by foo.bar().

$\endgroup$
9
  • $\begingroup$ Thanks a lot for your answer! I think the "resource closing" issue is best resolved in another way (always close if it goes out of scope, unless re-assigned; similar to a unique pointer). The "only catch some exception" can be resolved using an explicit scope by adding eg. "if true", if that should be needed. But I value very much your opinion! $\endgroup$ Commented Jun 27 at 11:48
  • 1
    $\begingroup$ @ThomasMueller The issue is not generally about closing the resource, but rather that you don't want to handle an exception thrown when opening the resource. For example, if the resource is a database, you often want to return a specific error from the current function if the current query fails, but if connecting to the database fails then you want the exception to bubble up. Closing is handled automatically in constructs like Java's try-with or Python's with, but note how in try(Foo foo = acquireFoo()) { ... }, an exception thrown by acquireFoo() isn't caught locally. $\endgroup$
    – kaya3
    Commented Jun 27 at 12:08
  • $\begingroup$ I see! For this case, there are two options: one is to have separate catch blocks for the "open" and "use" parts (in which case no indentation is needed at all), or then define a separate scope for the "use" part. But still, I think try is not needed (more harmful than helpful). $\endgroup$ Commented Jun 27 at 12:26
  • 2
    $\begingroup$ @ThomasMueller If it's possible to have more than one catch within a scope, then you have an equivalent of a try block, even if it's defined implicitly as "all lines from start-of-scope to catch, or from catch to next catch". Whether those blocks are indented is in most languages up to the user anyway, and they'll probably naturally indent anything that "feels like" a scope - for instance, the lines between case and break in a C-style switch statement aren't technically a block, but most people will indent them as though they were. $\endgroup$
    – IMSoP
    Commented Jun 27 at 14:16
  • 1
    $\begingroup$ "People will forget to do this." -- similar to the problems that occur when people don't put the single-statement body of if in braces. stackoverflow.com/questions/359732/… $\endgroup$
    – Barmar
    Commented Jun 27 at 16:52
2
$\begingroup$

The try in modern languages is just for the improvement of readability. If you look at it from a language processing perspective, the parse rules is usually something like this: <try> block() <catch> handler() Here, it does not really matter what block or handler rules really look like. Removing the try keyword would result in a transformation of the block rule users to become essentially block() (<catch> handler())?. And AST processing would have to check for the option and create the handler whenever a catch block is present.

Ada does it, example: https://learn.adacore.com/courses/intro-to-ada/chapters/exceptions.html#handling-an-exception

Ada is one of the oldest languages. So, we can assume that most language authors who were aware of it decided against it because they noticed that it reduces the readability of code if the target block is too large. If you look at code that doesn't fit onto the screen anymore and you lack the try in your language, you will be made aware of the catch only after reading a lot of code when reading it top down.

Go's defer/recover approach is also not using a try. Evaluating it can only be done in the light of Go's view on exceptions that essentially is that exception handling should not be used at all. It is a good way of preventing programmers coming from other languages to continue using exception handling for handling exceptional situations.

Regarding the throws declaration that is part of the current form the question, I'd suggest splitting it to another question. Nonetheless, a similar concept exists in both C++ and Java. It should be discussed in a separate question because the answer interacts with other concepts in the language and the presence of implicit exceptions.

$\endgroup$
1
$\begingroup$

Most answers, and perhaps the OP as well, assume that there still is a well-defined scope on which the catch handler acts. However, there are languages which allow you to define exception handlers that work globally. Consider some variants of BASIC which have:

ON ERROR GOSUB <line>

Note that this statement can install an exception handler for exceptions occuring outside of the scope of this statement itself.

The problem with this approach is that exception handling is not composable. So tying it to a scope, whether implicit or explicit with a try keyword, is preferrable.

An issue that arises when you don't have an explicit scope is how you define the implicit scope in an intuitive and robust way. What do you expect happens in the following case, when foo() does not throw but bar() does?

foo()
catch e:
    handle_exception(e)
bar()

You could say that catch only catches exceptions from code that came before it. But then consider that not all code is a linear sequence:

for i in range(10):
    foo()
    catch e:
        handle_exception(e)
    bar()

Does it catch exceptions from the second invocation of bar()? Without a try, catch looks like a regular statement. What if someone wants to make error handling depend on something else?

foo()
if bar:
    catch e:
        handle_exception(e)

Now one must really know the rules to be able to tell if the catch will still apply to exceptions thrown by foo(), or if it's a no-op because its scope is only the body of the if-statement.

$\endgroup$
3
  • $\begingroup$ > "Does it catch exceptions from the second invocation of bar()?" I think that depends on the specification. I would argue that exceptions in bar() are not caught, unless the "catch" part is moved to be lower. My personal feeling is that "knowing the rules" is not terribly complicated in this area, and the complexity is similar to the question "what is the lifetime of a variable?". The answer is not the same, but the complexity is similar. $\endgroup$ Commented Jun 30 at 12:14
  • $\begingroup$ foo() if bar / catch. There is no "conditional catch" in other languages I'm aware of. But it is simple to say "catch e if bar handle_exception(e)". $\endgroup$ Commented Jun 30 at 12:19
  • 1
    $\begingroup$ I personally agree with all that, but that's because of intuition from using other programming languages. It might not be obvious to everyone, and when designing a new programming language, it's also not something you have to blindly copy. If you can make something better by not following the usual conventions, that would be very interesting :) $\endgroup$
    – G. Sliepen
    Commented Jun 30 at 12:48
1
$\begingroup$

Trying this out in practice with C3 some years ago. I found that it was actually less clear than I expected – plus it didn't yield any additional useful semantics nor flexibility over regular try/catch handling.

Even if the try is explicitly stated in some way, like

a := try foo()
doSomething(a)
catch e
  ...error_handling...
  • it's still difficult to make out the error flow clearly.

(In the end, C3 got a fairly unique blend of implicitly flatmapped Result/Optional, but that's a different story)

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .