Is there a better way to do error handling on Julia than try/catch?


#1

Julia could make error types by using Union or maybe a la Golang with tuples, why does it choose to use the terrible approach that is to treat errors as exceptions?. To differentiate this brings better logic flow to the code as the case for Elixir or Golang that handle errors and exceptions pretty different


#2

You can certainly choose any of those patterns in your own code, since Julia already supports multiple return values (or, really, returning and unpacking tuples) and various kinds of option types like Some{T} and Union{T, Nothing}.

This kind of tone is not likely to lead to a productive or helpful discussion in a forum like this. Julia is the product of a lot of design choices, none of which are perfect, but calling something a “terrible approach” is more likely to lead to pointless bickering than to an actual discussion about how to improve the language.


#3

I think Julia and especially its type system combined with multiple dispatch are really powerful features which allow you to implement whatever error handling design you like.
This starts with basic try/catch, over things like Robin mentioned (optionals/unions) and goes as far as implementing your own monads like you do in e.g. Haskell (with the tiny difference that in Julia you don’t really have to worry about speed :wink: ).


#4

You might be interested in ResultTypes.jl.


#5

I did not mean to offend but it is not the best error handling


#6

It is one approach to error handling, which is useful in some situations, less so in others. As @rdeits said, Julia is flexible enough to allow you to experiment with other approaches. Many functions in Base and in packages have a form that uses a type union instead of throwing an error, eg Base.tryparse, you can of course follow this pattern with your own functions.


#7

What’s wrong with try/catch? Serious question.


#8

The problem is mostly practical:

  1. it can be costly, even if the exception is not thrown,
  2. some tools cannot handle it (eg Zygote.jl#88)

That said, I would not say that there is anything wrong with try ...catch per se. It is just a tool from a palette of tools, and if someone wants to use exceptions for control flow, they should just use another tool.


#9

After looking at https://www.youtube.com/watch?v=PH4WBuE1BHI&t=2383s (and before learning about ResultTypes.jl) I made my own version of errors as return values https://github.com/KristofferC/Expect.jl.


#10

Did you actually end up using it in practice?


#11

Nope, I didn’t feel it solved any problem I had.


#12

In practice it feels like the choice in Base is to return nothing when there is no sensible answer, for example tryparse returns nothing if things can’t be parsed, or findfirst findlast return nothing if no value is found.

Base aslo provides some tools to work with nothing as a return value, i.e. something(x, val) (to use default val value if x is nothing), or isnothing(x) ? do_one_thing : do_another_thing. I think more syntactic sugar for this is planned: things like a ?? b for something(a, b), see https://github.com/JuliaLang/julia/issues/26303, or T? for Union{T, Nothing}.


#13

Exceptions are just a control flow primitive for “unwind the call stack” and have inherently nothing to do with error handling. This control flow primitive is occasionally very useful (but remember that it is often slow).

Whether to use this control flow primitive at all, and for what purpose, is up to you.

Most of julia only throws exceptions for things that should be considered programmer error.


#14

To this point, do you have a sense for how expensive exceptions handling (when not thrown) are in practice? We do use them for things handling parsing errors thrown by the Base code, but we could write our own parsers if that is likely to be much faster. Thanks,


#15

You should benchmark your code, bit in general it is somewhat costly, even if you don’t throw the exception. Eg

using BenchmarkTools

function tryparse2(T, str)
    try
        parse(T, str)
    catch
        nothing
    end
end

xs = randn(1000);
all_valid = string.(xs)
some_invalid = copy(all_valid)
some_invalid[1:10:end] .= "XXX"

benchmarks as

julia> @btime tryparse.($Ref(Float64), $all_valid);
  261.352 μs (1495 allocations: 31.33 KiB)

julia> @btime tryparse2.($Ref(Float64), $all_valid);
  436.388 μs (1495 allocations: 31.33 KiB)

julia> @btime tryparse.($Ref(Float64), $some_invalid);
  341.909 μs (1398 allocations: 30.88 KiB)

julia> @btime tryparse2.($Ref(Float64), $some_invalid);
  5.505 ms (2898 allocations: 102.75 KiB)

#16

The other general question I have about try/catch and exceptions is whether using things like

function f(x, a)
    @assert a > 0
    return sqrt(a) * x
end

etc. for testing preconditions in the code (e.g. a parameter must be a > 0) where I wouldn’t specifically do not want recover and “handle” the errror. Does this introduce all of the same possible possible performance issues and incompatibility with Zygote?

BTW, I sure hope that there is a way to turn off @assert at some point as a global setting when launching Julia, or an alternative way to state a bunch of preconditions you only want to check while doing development.


#17

Exceptions that are not thrown are very cheap (the branches leading to them only prevent certain compiler optimizations).

Setting up an exception handler (the try/catch block) has a certain overhead, and actually throwing them is expensive.

For things like parsing, you should have an outer entry-function that sets up exception handlers, and then calls your internal, likely recursive, parsing functions. That way, you only pay once for the exception handler. This is exactly what exceptions are good for: You need to return to the entry-point of your entire parser, from a dynamic depth of the callstack. Setting up new nested try/catch blocks (i.e. a deep stack of exception handlers) is both slow and defies the point of using exceptions instead of signaling failures via return values.


#18

Is it slow due to the current implementation? Could it be optimized somehow?

I question about this general statement only because Java apps often deals with layers and layers of exception handling and it’s not slow.


#19

Slow is a relative term. What do you refer to by “it’s not slow”? I think of creating exceptions in Java as very slow, and for me that means several microseconds. The costly part is unwinding the call stack and filling in the stack trace. (And in Java production code I’ve seen, the stack depth can be huge.)

I haven’t benchmarked creation and throwing of exceptions in Java vs Julia. To me that doesn’t feel very relevant. You don’t rely on exceptions in performance critical code. Period.

I think that most recommendations about exceptions in Java also apply to Julia: Use exceptions for exceptional conditions, not for regular control flow. Don’t ignore exceptions. Include values of parameters in the exception message. Etc.


#20

You’re right. My comment about Java exception handling not being slow is indeed somewhat misguided. Apparently, nothing’s is free when it comes to catching and collecting the stack trace as discussed here, here, and here