-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Description
A of the major issues with try/catch is that it's possible to catch the wrong error and think that you are handling a mundane, expected problem, when in fact something far worse is wrong and the problem is an unexpected one that should terminate your program. Partly for this reason, Julia discourages the use of try/catch and tries to provide APIs where you can check for situations that may cause problems without actually raising an exception. That way try/catch is really left primarily as a way to trap all errors and make sure your program continues as best it can.
This is also why we have not introduced any sort of typed catch clause into the language. This proposal introduces typed catch clauses, but they only catch errors of that type under certain circumstances in a way that's designed to avoid the problem of accidentally catching errors from an unexpected location that really should terminate the program. Here's what a Java-inspired typed catch clause might look like in Julia:
try
# nonsense
foo(1,2,3) # throws BazError
# shenanigans
catch err::BazError
# deal with BazError only
endOf course, we don't have this in Julia, but it can be simulated like this:
try
# nonsense
foo(1,2,3) # throws BazError
# shenanigans
catch err
isa(err, BazError) || rethrow(err)
# deal with BazError only
endAlthough this does limit the kinds of errors one might catch, it fundamentally doesn't solve the problem, which is that while there are some places where you may be expecting a BazError to be thrown, other code that you're calling inside the try block might also throw a BazError where you didn't expect it, in which case you shouldn't try to handle because you'll just be covering up a real, unexpected problem. This problem is especially bad in the generic programming contexts where you don't really know what kind of code a given function call might end up running.
To address this, I'm proposing adding a hypothetical throws keyword that allows you to annotate function calls with a type that they're expected to throw and which you can catch with a type catch clause (it's a little more subtle than this, but bear with me). The above example would be written like this:
try
# nonsense
foo(1,2,3) throws BazError
# shenanigans
catch err::BazError
# deal with *expected* BazError only
endThe only difference is that the throws BazError comment after the call to foo became syntax. So the question is what does this annotation do? The answer is that without that annotation indicating that you expect the expression to throw an error of type BazError, you can't catch it with a typed catch. In the original version where the throws BazError was just a comment, the typed catch block would not catch a BazError thrown by the call to foo – because the lack of annotation implies that such an error is unxepected.
There is a bit more, however: the foo function also has to annotate the point where the BazError might come from. So, let's say you had these two definitions:
Consider something like this:
function foo1(x,y,z)
bar(x,2y) throws BazError
end
function foo2(x,y,z)
bar(x,2y)
end
function bar(a,b)
throw BazError()
endThis also introduce a hypothetical keyword form of throw – more on that below. These definitions result in the following behavior:
try
# nonsense
foo1(1,2,3) throws BazError
# shenanigans
catch err::BazError
# error from bar is caught
end
try
# nonsense
foo2(1,2,3) throws BazError
# shenanigans
catch err::BazError
# error from bar is NOT caught
endThe rule is that a typed catch only catches the an error if every single call site from the current function down to the one that actually throws the error is annotated with throws ErrorType where the actual thrown error object is an instance of ErrorType. An untyped catch still catches all errors, leaving the existing behavior unchanged:
try foo1(1,2,3)
catch
# error is caught
end
try foo2(1,2,3)
catch
# error is caught
endSo what is the rationale behind this proposal and why is it any better than just having typed catch clauses? The key point is that in order to do a typed catch, there has to be a "chain of custody" from the throw site all the way up to the catch site, and each step has to expect getting the kind of type that's thrown. There are two ways to catch the wrong error:
- catch an error that was not the type of error you were expecting
- catch an error that came from a place you weren't expecting it to come from
The first kind of mistake is prevented by giving an error type, while the second kind of mistake is prevented by the "chain of custody" between the throw site and the catch site.
Another way of thinking about this is that the throws ErrorType annotations are a way of making what exceptions a function throws part of its official type behavior. This is akin to how Java puts throws ErrorType after the signature of the function. But in Java it's a usability disaster because you are required to have the throws annotation as soon as you do something that could throw some kind of non-runtime exception. In practice, this is so annoying that it's pretty common to just raise RuntimeErrors instead of changing all the type signatures in your whole system. Instead of making runtime excpetion vs. non-runtime exception a property of the error types as Java does, this proposal makes it a property of how the error occurs. If an error is occurs in an expected way, then it's a non-runtime exception and you can catch it with a typed catch clause. If an error occurs in an unexpected way, then it's a runtime exception and you can only catch it with an untyped catch clause. You can only catch errors by type if they are part of the "official behavior" of the function you're calling, where official behavior is indicated with throws ErrorType annotations.
One detail of this proposal that I haven't mentioned yet is that throw would become a keyword instead of just a function. The reason for this is that writing
throw(BazError()) throws BazErrorseems awfully verbose and redundant. For compatibility, we could allow throw() to still invoke the function throw while throw ErrorType(args...) would be translated to
throw(ErrorType(args...)) throws ErrorTypeThis brings up another issue – what scope should the right-hand-side of throws be evaluated in? In one sense, it really only makes sense to evaluate it in the same scope that the function signature is evalutated in. However, this is likely to be confusing since all other expressions inside of function bodies are evaluated in the local scope of the function. It would be feasible to do this too, and just rely on the fact that most of the time it will be possible to statically evaluate what type expression produces. Of course, when that isn't possible, still be necessary to emit code that will do the right thing depending on the runtime value of the expression. If the r-h-s is evaluated in local scope, then we can say that throws expr is equivalent to this:
throw(expr) throws typeof(expr)Usually it will be possible to staticly determine what typeof(expr) is, and it is a simpler approach to explain than restricting the syntax of the expression after the throw keyword.
Note that under this proposal, these are not the same:
try
# stuff
catch
# catches any error
end
try
# stuff
catch ::Exception
# only catches "official" errors
endAlso note that this proposal is, other than syntax additions that are not likely to cause big problems, completely backwards compatible: existing untyped catch clauses continue to work the way they used to – they catch everything. It would only be new typed catch clauses that only catch exceptions that have an unbroken "chain of custody".