Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support catching specific exception types #23418

Open
quinnj opened this issue Aug 24, 2017 · 17 comments
Open

Support catching specific exception types #23418

quinnj opened this issue Aug 24, 2017 · 17 comments
Labels
domain:error handling Handling of exceptions by Julia or the user

Comments

@quinnj
Copy link
Member

quinnj commented Aug 24, 2017

A la

try
    foo()
catch e::ArgumentError
    # hey, I know how to recover from an ArgumentError!
catch e::AssertionError
    # I also know how to recover from an AssertionError!
catch e
    # hmmm, anything else is probably fatal
    rethrow(e)
end

I know there have been (hotly debated) grand visions of error-handling laid out in #7026 and #15514, but as far as julia 1.0, I think we should at least support catching specific errors. My latest methodology for error-handling goes something like:

  • Be aware of all possible exceptions thrown by your code and which methods throw them
  • For each method that might throw, and for each different exception type
    • Decide if there's some kind of recoverable action due to that specific exception type (retry, clean something up & try plan B, etc.)
    • If there's no possibility to recover, we need to rethrow, but possibly as a new exception type with additional context info from where I'm try-catching
  • Clearly document the types of exceptions that can be thrown from my own methods; what and why each is thrown

The only issue w/ this approach is I end up w/ code like:

try
    foo()
catch e
    typeof(e) <: ArgumentError &&  recover_from_argumenterror() # hey, I know how to recover from an ArgumentError!
    typeof(e) <: AssertionError && recover_from_assertionerror() # I also know how to recover from an AssertionError!
    # hmmm, anything else is probably fatal
    rethrow(e)
end

which is decidedly less pleasant. I feel like having just a little extra syntax could go a long way to making error-handling feel a little more grown up in julia.

Any idea on how hard this would be? Where to start looking to implement?

@JeffBezanson
Copy link
Sponsor Member

Duplicate of #1075.

If we do this, it could be implemented entirely in the front end. First the parser would have to be modified to accept multiple catch blocks, then these would be lowered into if statements and rethrow.

@samoconnor
Copy link
Contributor

samoconnor commented Aug 24, 2017

What if instead of catch e::type we have catch f(e) -> ::Bool or catch e if Bool expr

try
    foo()
catch e -> e isa NetworkError
   ...
catch e -> e isa HTTPError && e.code == 404
    ...
end
try
    foo()
catch e if e isa NetworkError
   ...
catch e if e isa HTTPError && e.code == 404
    ...
end

Catching on a Bool function (or expression) is more flexible than catching on types, and not very much more verbose for cases where the expression is just e isa FooType

Also can we have a retry keyword too? :)
See https://github.com/samoconnor/Retry.jl

[updated: changed :> to isa, thanks @quinnj]

@samoconnor
Copy link
Contributor

Related #15906

@kshyatt kshyatt added the domain:error handling Handling of exceptions by Julia or the user label Aug 24, 2017
@quinnj
Copy link
Member Author

quinnj commented Aug 24, 2017

The problem in your example @samoconnor is you're missing e as a value and a type: i.e. e <: NetworkError suggests e is a type, whereas e.code == 404 assumes it's the object itself. I say we keep this as simple as possible to make catching specific exception types cleaner. e::SpecificExceptionType is nice because it matches current behavior (and indeed, it looks like it's currently an error on master, which means it's not changing existing behavior). After you catch e::HTTPError, it seems reasonable to then do your own "business logic" within that specific catch block. I'm not aware of any other language that allows full on boolean expressions when catching.

@samoconnor
Copy link
Contributor

@quinnj thanks for spotting e <:, my intention was e isa, I'll update the examples above...

@samoconnor
Copy link
Contributor

@JeffBezanson, @StefanKarpinski Here's another possibility with a smaller change and the same intention of reducing unintended over-catching....

What if rethrow(::Exception) was removed and replaced with its opposite delete!(::Exception).
i.e. catch would no longer consume exceptions by default, it would rethrow them unless deleted.

julia> try
           foo()
       catch e
           println("Whoops foo() said: $e")
       end
Whoops foo() said: UndefVarError(:foo)
ERROR: UndefVarError: foo not defined

vs

julia> try
           foo()
       catch e
           println("Whoops foo() said: $e")
           delete!(e)
       end
Whoops foo() said: UndefVarError(:foo)

The example above would become:

try
    foo()
catch e
    if e isa NetworkError
        ...
        delete!(e)
    elseif e isa HTTPError && e.code == 404
        ...
        delete!(e)
    end
end

With this feature you might change #15906 to be "Undeleteable exceptions"...

try
    foo()
catch e
    delete!(e)
end
ERROR: OutOfMemoryError is not deletable. 

(Maybe delete(e, force=true) could be supported too.)

@samoconnor
Copy link
Contributor

@quinnj I feel pretty strongly that catch-by-type is a half-way solution, and that we should be able to do better than what Java did 20 years ago.
As an example, with AWS stuff I end up with lots of non-type filtering like this:

if e.code in ["404", "NoSuchKey", "AccessDenied"]

@quinnj
Copy link
Member Author

quinnj commented Aug 24, 2017

But I feel like you're just hijacking this feature request, which was intended to be minimal and hopefully easily accepted. You're basically just reproposing #15906, which you can and should discuss on those threads.

I'd rather we at least do the simple thing first, which, again, should hopefully be easily accepted by everyone before we try re-inventing error-handling. Don't get me wrong, grand visions and creative functionality have their place, but I'm just trying to push for a basic language feature that most other languages support.

@samoconnor
Copy link
Contributor

Sorry @quinnj, I didn't mean to make you feel like your request was hijacked.

@quinnj
Copy link
Member Author

quinnj commented Aug 24, 2017

No worries. I understand you're passionate about error-handling and that's great. I'm just worried about a simple feature request turning into #7026 or #15514, which were much more hotly debated. I'm hoping for a "quick win" here.

@theAkito
Copy link

theAkito commented Dec 4, 2019

How is progress on this subject?

@pyrsmk
Copy link

pyrsmk commented Jul 4, 2020

Yeah, this is crazy that such a feature is not already supported...

@heetbeet
Copy link

heetbeet commented Feb 3, 2021

I have created this workaround in the mean time (will be available after the 3 day package registration period):
https://github.com/heetbeet/TryCatch.jl

@Tikquuss
Copy link

Not yet available? 👀👀

@StefanKarpinski
Copy link
Sponsor Member

Y'all are commenting as though it is decided that this should be done and it simply hasn't been implemented. That is not the case. It is unclear that we want this.

That said, I've come to favor it because I realized at some point that it doesn't actually clash with the "chain of custody" idea I proposed long ago since they can be distinguished by whether there are throws annotations in the try block or not. So we could have this and then have a more precise catching mechanism as well when calls have throws annotations.

@dhanak
Copy link

dhanak commented Jul 21, 2023

Just for reference: not having found this issue here, I proposed the exact same modification to the try-catch syntax on discourse. Some thoughts and ideas could crop up there as well.

I'm definitely in favor of this change.

@mkitti
Copy link
Contributor

mkitti commented Jul 22, 2023

I came up with a macro that makes it easier to create an anonymous error handler using multiple dispatch rather than building an if/else statement.

https://discourse.julialang.org/t/new-syntax-suggestion-catch-with-type-specs/101840/22?u=mkitti

macro error_dispatch(e)
     _error_dispatch(e)
end
function _error_dispatch(ex)
    catch_block = ex.args[3]
    exception = ex.args[2]
    catch_func = gensym(:catch)
    # map anonymous functions to methods of a gensym named function
    catch_block.args = map(catch_block.args) do cex
        if cex isa Expr && cex.head == :(->)
            _anon_to_named_func(catch_func, cex)
        else
            cex
        end
    end
    # call named function
    push!(catch_block.args, :($catch_func($exception)))
    esc(ex)
end

# convert anonymous functions to method of named function
function _anon_to_named_func(name::Symbol, anon::Expr)
    @assert(anon.head == :(->))
    func_args = anon.args[1]
    func_body = anon.args[2]
    quote
        $name($func_args) = $func_body
    end
end

The usage looks like this:

julia> function foo(g)
           @error_dispatch try
               g()
           catch e
               e::AssertionError -> println("Hello. I got an AssertionError!")
               e::InexactError -> println("Hola. ¡Recibí un InexactError!")
           end
       end
foo (generic function with 1 method)

julia> foo(()->@assert(false))
Hello. I got an AssertionError!

julia> foo(()->Int(5.2))
Hola. ¡Recibí un InexactError!

The macro expansion is as follows.

julia> @macroexpand @error_dispatch try
           g()
       catch e
           e::AssertionError -> println("Hello. I got an AssertionError!")
           e::InexactError -> println("Hola. ¡Recibí un IneaxctError!")
           e -> println("Unexpected Error")
       end
:(try
      #= REPL[90]:2 =#
      g()
  catch e
      #= REPL[90]:4 =#
      begin
          #= REPL[74]:6 =#
          var"##catch#315"(e::AssertionError) = begin
                  #= REPL[74]:6 =#
                  begin
                      #= REPL[90]:4 =#
                      println("Hello. I got an AssertionError!")
                  end
              end
      end
      #= REPL[90]:5 =#
      begin
          #= REPL[74]:6 =#
          var"##catch#315"(e::InexactError) = begin
                  #= REPL[74]:6 =#
                  begin
                      #= REPL[90]:5 =#
                      println("Hola. ¡Recibí un IneaxctError!")
                  end
              end
      end
      #= REPL[90]:6 =#
      begin
          #= REPL[74]:6 =#
          var"##catch#315"(e) = begin
                  #= REPL[74]:6 =#
                  begin
                      #= REPL[90]:6 =#
                      println("Unexpected Error")
                  end
              end
      end
      var"##catch#315"(e)
  end)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:error handling Handling of exceptions by Julia or the user
Projects
None yet
Development

No branches or pull requests