Please stop using `error` and `ErrorException` in packages (and Base)

In julia we like to distinguish different things using different types (exceptions are one such thing).
In most languages they also like to distinguish different exceptions with different types.

In particular in julia

try
    foo()
catch err
    if err isa BarError
            # Handle it
    elseif err isa BlargError
            # Handle that
    else
           rethrow()
    end
end

Saying error("it is borked because the Blarg is flumoxed") causes an ErrorException to be thrown.
This is an incredibly unhelpful error, typewise.
You can;t work out what threw it or why.
So you canā€™t handle it safely.

error exists for when you canā€™t be bothered declaring an exception type.
Or working out which existing one to use.
It belongs in user-scripts, and quick prototypes.
It doesnā€™t belong in registered packages, and it particularly doesnā€™t belong in Standard Libraries / Base
Most gone now. However, the run command still throws one (https://github.com/JuliaLang/julia/pull/27900).

I suggest that ErrorException should be treated as if it were an uncatchable exception type.

I suspect our over reliance on using error might come back to prior experiences we have using other languages where declaring your own exception type is difficult.
But in julia it is easy

struct MyError <:Exception end
or

struct MyErrorWithMsg <:Exception
    msg::String
end

There is also @assert for things that a programmer errors.
So you can replace

if val == true
     # do it
elseif val==false
     # do not do it
else
     error("This should never happen") 
end

With

if val == true
     # do it
elseif val==false
     # do not do it
else
     @assert(false, "This should never happen") 
end

Which is clearer.


I am not saying I am perfect, I have packages where I was lazy and used error,
rather than using the right exception type.
But lets try and clean those out now, while we are getting ready for 1.0.

Save error for end-users; keep it out of your packages

22 Likes

One solution which involves changing the standard library might be to

  • make a add a type param for ErrorException{N} <: Exception in Base.
  • deprecate error for @error
  • define @error(msg) to do throw(ErrorException{nameof(@__MODULE__)}(msg)

At least then it would be apparent (in the type) what module was throwing an error.
Also using @error would have nice symmetry with @warn and @info

I think this is a non-breaking change? Since ErrorException{N} <: ErrorException.
Maybe not since they are not equal. It is an almost nonbreaking change

Still not as good as throwing errors with semantic types

5 Likes

I tend to use exceptions to propagate information towards the user, not for control flow. So while I see the value of fine-grained exceptions, especially if running in an application which can decide what to handle and what to fail on, I am curious what your use case is for enforcing a fine-grained distinction.

As for the occasional ErrorException, I find the stack trace has the information I need.

1 Like

I see the value of fine-grained exceptions, especially if running in an application which can decide what to handle and what to fail on

Thing is, when you are writing a package you donā€™t know if the package is being used by a project which can decide what to handle and what to fail on.

Packages are for reuse.
That is why I am saying in packages, (not nesc in general),
avoid using error.
In a application (in the Pkg3 sense) go wild, use what ever error handling you want.

Like yes, the stack information always has what you need,
and the message often also has useful info.
But we donā€™t want to be doing exception handling via programatically inspecting the stack, or regexing the message

3 Likes

I agree, but I expect that if someone depends on handling exceptions, this part of the API can be always be refined as a feature request.

I think it is good to know the exception types in Base and use them when applicable, but in case they donā€™t fit, I am not sure I would introduce a new exception type unless there is a strong reason to do so.

Also, categorizing an exception can be a bit fuzzy. Eg there is considerable overlap between ArgumentError and DomainError, so very fine-grained handling may be tricky.

2 Likes

Of-course. I do appreciate feature-request driven development (particularly as a method to avoid gold-plating)
It is worth remembering that changing your exception types is a breaking change.
So it is nice to be proactive and get it right the first time.

I think it is good to know the exception types in Base and use them when applicable, but in case they donā€™t fit, I am not sure I would introduce a new exception type unless there is a strong reason to do so.

Can you expand on why you are not sure introducing new exceptions is good?

Also, categorizing an exception can be a bit fuzzy. Eg there is considerable overlap between ArgumentError and DomainError, so very fine-grained handling may be tricky.

Indeed, though IIRC in the particular case it was actually purely an optimization thing. Up until fairly recent compiler versions there was an overhead to having a field in your exception, that was hurting critical inner loop somewhere in Base. So DomainError unlike ArgumentErrror had no message field.
Anyway I maintain either is better than ErrorException since they tell you something is wrong with the functions inputs
Can always handle both via || or Union

1 Like

See
https://github.com/JuliaLang/julia/issues/7026
for a lot of discussion on this. Coming from C# I also thought about using exceptions in Julia for control flow but it seems that there is no consensus on this being Julian.

1 Like

If someone wants to dispatch on it, I would certainly introduce a type, but I would wait until then. There is no strong reason not to do so. Unless I am 90% confident that I need to dispatch on it, I consider a specific exception type premature. There are already

julia> length(subtypes(Exception))
58

in Base alone.

1 Like

Unless I am 90% confident that I need to dispatch on it, I consider a specific exception type premature.

I think we are just going to have to disagree on that one.
And that is Ok. :slight_smile:

julia> length(subtypes(Exception))
58

For reference in 0.7, there are only 27 exported Exceptions in Base and core
It is worth excluding the ones that are not exported.

All exceptions

julia> exceptionnames = Set(nameof.(subtypes(Exception)))
Set(Symbol[:SystemError, :PkgTestError, :DimensionMismatch, :ErrorException, :InexactError, :OverflowError, :UndefRefError, :UndefVarError, :CapturedException, :CodePointError  ā€¦  :ArgumentError, :WrappedException, :BatchProcessingError, :MissingException, :CommandError, :UVError, :SimdError, :PkgError, :PosDefException, :OutOfMemoryError, :ReadOnlyMemoryError])

julia> length(ans)
55

counting exported and non-exported names from Base and Core

julia> exceptionnames āˆ© (Set(names(Base; all=false)) āˆŖ Set(names(Core; all=false)))
Set(Symbol[:SystemError, :InterruptException, :DivideError, :DimensionMismatch, :KeyError, :ErrorException, :InexactError, :TypeError, :MethodError, :SegmentationFault  ā€¦  :UndefVarError, :CapturedException, :InvalidStateException, :AssertionError, :StringIndexError, :StackOverflowError, :OutOfMemoryError, :UndefKeywordError, :EOFError, :BoundsError, :ReadOnlyMemoryError])

julia> length(ans)
27

btw, whatā€™s the difference between error and exception?

error is a function that corresponds to error(msg) = throw(ErrorException(msg)).

Exception is an abstract type, that is the parent (/ancestor) of all exception types,
including ErrorException, as well as say BoundsError etc.

I mean, there are SystemError and InterruptException

Nothing, there is no meaningful difference/consistent convention.
It might be nice if there was, but there isnā€™t.

1 Like

Yeah, SegmentationFault even not error nor exceptionā€¦

(for error vs exception, just found a section Errors and exceptions in Exception Class (System) | Microsoft Docs)

1 Like

Iā€™m wondering if you have some good examples of errors thrown where it makes sense to use different exception types and then have code handling those.

Right now, I can only think of cases where you actually use exceptions as control flow - of which Iā€™m not a big fan. There are also the cases for cutting down down on boilerplate (e.g. out of bounds error follow a certain structure + print out, which can be implemented one time as part of the BoundsError type) - but that one is missing the handling of the exception part.

I consider most of my errors as fatal and follow the philosophy of only checking/catching errors where they occur.
Exception to this is test code: code is easier to test for certain errors, when the different exceptions that can occur have different types, instead of checking the string of the ErrorException. But only working with ErrorException seems acceptable albeit less clean, since even if you use different types for the exceptions, you should still check the message :wink:

3 Likes

I just scanned a few of my packages for error() calls (which I use extensively) and found several use cases:

  1. Programmer error. Could be replaced with @assert, but I donā€™t really see the difference since both are essentially unrecoverable.
  2. Bad arguments, but not user fault. For example, someone can try to differentiate function that we donā€™t currently support.
  3. Bad environment. E.g. trying to load binary dependency that is missing from the system (assuming the package canā€™t be used without it).

In all these cases I want an error message to be propagated directly to the user, so ErrorException is used to indicate that something went terribly wrong and there isnā€™t much you can automatically do about it.

On the other hand, I can clearly see the advantage of using specific exception types when:

  1. Working with external resources, e.g. missing file, failing network or bad user input are all recoverable.
  2. For control flow, e.g. to skip bad records in a list or stop doing writing to a storage when itā€™s full (although Iā€™d really love if we had something like Common Lispā€™s condition system).
1 Like

I think that the idiomatic way to do this in Julia is using small unions, not unlike tryparse.

It only works when control decision is taken exactly one level higher than the function returning a small union, e.g.:

function sum_numbers(lines)
    s = 0
    for line in lines
        res = tryparse(Int, line)
        if res != nothing
            s += res
        end
    end
    return s
end

But itā€™s less convenient when you have several levels of invocation, e.g.:

function process_dir(dirname)
    processed_files = 0
    for file in readdir(dirname)
        file_ok = process_file(file)
        if file_ok 
            processed_files += 1
       end
    end
    println("Processed $processed_files files")
end

function process_file(file)
    lines = ...
    failed = false
    for line in lines
        res = tryparse(line)
        if res != nothing
            ...
        else
            failed = true
            break 
        end
    end
    return !failed
end

which can be simplified to:

function process_dir(dirname)
    processed_files = 0
    for file in readdir(dirname)
        # we still need a check on the control flow level
        try
            process_file(file)
            processed_files += 1
       catch e
           if e isa ArgumentError
               # ignore
           else
               rethrow()
           end
       end
    end
    println("Processed $processed_files files")
end

function process_file(file)
    lines = ...
    for line in lines
        # we don't need special checks in intermediate levels since exception  
        # will unroll stack anyway 
        res = parse(line)
        ...
    end
end

I think one of the problems for me at least is that it can be hard to know what the appropriate exception type is. I find myself using ArgumentError quite oftenā€¦ in fact Iā€™d go as far as to say usually if I bother to explicitly throw an exception itā€™s an ArgumentError.

For me this problem is in no way specific to Julia. I also tend to really dislike try catch constrcuts, I usually feel that either my code should run without error or it should not run at all.

3 Likes

For both these cases, it is easy to imagine situations where I might want to catch the exception:

For missing binary dependencies, the package that depends on your package might provide a workaround, so that the end-user does not need to be bothered. Or it might want to rewrite the error message, because the end-user does not want to know about the nitty gritty internals of your package.

Your example with differentiation is even more salient: A package that uses differentials might want to try multiple strategies; e.g. try symbolic before AD, but only if symbolic (1) works and (2) the expressions stay reasonably small; so your user, who is a package author, may reasonably want to catch this.

An example I dealt with very recently are bad llvm instructions: On archlinux 0.6.3 binaries, I get the unrecoverable (crash julia) LLVM ERROR: Cannot select: intrinsic %llvm.x86.aesni.aeskeygenassist. You know, Iā€™d like to catch that and fall back on a software implementation. And ā€œthis version of llvm does not know whether your CPU supports whatever you are trying to doā€ looks like the same ballpark of error as your examples. (but making this catchable is probably harder than julia-issued ā€œerrorā€, as evidenced by julia crashing)

1 Like