New syntax suggestion: catch with type specs

Hello!

When using exceptions and try-catch blocks, it’s very common to handle some specific types of exceptions, and propagate everything else. Here’s how this is usually done:

try
    # body with potential exceptions
catch e
    if e isa AssertionError
        # handle assertion errors here
    elseif e isa InexactError
        # handle conversion errors here
    else
        rethrow() # everything else is passed on
    end
end

For me (and possibly others with some background in functional programming, like myself), this seems a bit cumbersome. There must be a way to be able to write this in a nicer, more concise format, without ifs. I can, for example, introduce a helper function using type dispatch:

handle_error(e::AssertionError) = nothing # handle assertion errors here
handle_error(::InexactError) = nothing # handle conversion errors here
handle_error(_) = rethrow()

try
    # body with potential exceptions
catch e
    handle_error(e)
end

No more ifs, but now the handling code is somewhat separated from the location of the error. If I follow this thought, the following notation comes to my mind:

try
    # body with potential exceptions
catch e::AssertionError
    # handle assertion errors here
catch ::InexactError # note the missing variable
    # handle conversion errors here
end
# every other error is propagated (uncaught) implicitly

This could be pure syntactic sugar, of course, resolving to either of the two forms above. But it would make the code so much clearer, IMHO!

I tried to find a conversation on this topic, but I couldn’t find any (except for this ancient one), which took me by surprise.

So, WDYT?

5 Likes

I really like this, but I also seem to remember that this exact design was discussed several years ago. I think it was put on ice as “not important enough”, or something like that.

I think the suggestion is great, but I also miss a switch/case (not macro-based), so…

2 Likes

I think it would be trivial to implement this feature if Julia had built in pattern matching.

This is how Python works, and it’s one of the (very few) things I miss when working in Julia.

2 Likes

and even earlier:

TLDR: it was rejected in 2012 since rethrow() is semantically sufficient (this is just syntactic sugar), but in 2022 @StefanKarpinski is at least vaguely supportive. But someone needs to submit the PR, which would involve:

  1. Changing the parser to parse this syntax
  2. Changing the lowering to lower this syntax to a sequence of if … rethrow() … statements.
7 Likes

Yeah, that’s it.

Yes, and the same goes for switch/case. It’s unnecessary, just much nicer.

1 Like

I am opposed to this change because it encourages the use of exceptions for expected errors. IMO exceptions should be used for unexpected errors, and another system should be introduced for producing and handling failure values. Python uses exceptions for control flow; I don’t think Julia should.

Meanwhile switch/case is a primitive implementation of pattern matching, which has been implemented much more generally with @match.

For my use cases @match is overpowered and lacks the single interesting quality of switch/case, which is syntactical convenience. I can just as well use if/elseif/elseif then.

1 Like

What is the proposed switch/case syntax?

I don’t have one. I like Matlab’s, except it’s a shame to need two reserved keywords.

Matlab

switch n
    case -1
        disp('negative one')
    case 0
        disp('zero')
    case 1
        disp('positive one')
    otherwise
        disp('other value')
end

MLStyle

@match n begin
  -1 => display("negative one")
   0 => display("zero")
   1 => display("positive one")
   _ => display("other value")
end

I prefer the former, especially when each case has multiple lines of code.

2 Likes

I agree with you in principle, but the reality is that, without an Option Type or some other integrated Error Type system, a lot of code does throw and need to be caught. I even saw a discussion here not that long ago about the performance of checking hasmethod vs just running the function and catching the MethodError and the latter was better.

As a 2.0 design question, moving to unwrapping with a match makes a lot of sense, but as it stands, it would be really nice to unpack errors with a switch.

I guess you could get close with a macro using if else blocks?

Something like

@switch n if -1
      println("negative one")
  elseif 0
      println("zero")
  elseif 1
      println("positive one")
  else
      println("other value")
end

Yeah, that’s a bit better.

True, but id doesn’t. So I deliberately avoided mentioning pattern matching, and stuck to a construct that is fundamental to Julia: type based dispatch.

This would be an interesting topic to debate. I tend to disagree with you. First, if anything encourages the use of exceptions for expected errors it’s the library functions that signal errors using exceptions (such as missing files). Second, it is often the case that certain types of errors need to be handled as and where they occur (and in this case, I agree with you, using return values for this is better design), some other errors, however, only need to be addressed near the top level of the code base. Exceptions have been invented just for that. Third, the pattern I started my question with is already prevalent, my suggestion merely offers a nicer syntax for writing the same thing. Discouraging something by making it deliberately ugly is not the way to go, IMHO.

Thanks for the heads up, at least now I know where to look for it.

I believe hasmethod and others will become zero-cost in 1.10.

2 Likes

Regarding the type-based catch approach, its details need to be carefully thought out in light of GitHub - JuliaServices/ExceptionUnwrapping.jl: "I started by producing, and the rapping came second to that, because I wanted to fill out the beat." - Awkwafina and concerns voiced there.

Let’s figure out how to make this happen with a macro.

Here’s the closest valid syntax.

@errdispatch try
    # do something
catch e
    e::AssertionError -> println("AssertionError")
    e::InexactError -> println("InexactError")
end

To be continued…

2 Likes

Sorry for the tangential question, but how do you get a macro to keep capturing after a line break without a begin?