The goal of this proposal is put forth a concrete proposal for having methods declare which exceptions are part of its API. This has been discussed in JuliaLang/julia#7026 and various other venues, but because the discussion there is extensive, I'm creating this as a clean document to put forward a concrete proposal.
This proposal extends the return-type syntax with the ability to specify exceptions
as well, by using the special Except type (which is not a real type, instead a
type component, similar to Vararg). For example, you might write:
function getindex(a::Vector, i::Int)::Except{BoundsError}
end
It is also permissable to mix these annotations with ordinary return type annotations:
function getindex(a::Vector{T}, i::Int)::Except{T, BoundsError}
end
On the ABI side, these new exceptions do not unwind the stack and are instead returned as discriminated unions.
Before describing how to throw one of these described exceptions, let's see what happens if we just use these in ordinary julia code:
bar()::Except{ErrorException} = #= throws error, see below =#
function foo()
# No handling of declared exception. Gets turned into an ordinary try/catch
# exception at this point
bar()
end
juli> foo
Error: ErrorException
[1] foo
To propagate an exception, this proposal introduces a new postfix operator.
The operator works similar to the ? postfix operator in Pony
(https://tutorial.ponylang.io/expressions/errors.html).
Notionally the syntax I'll be using is !?, but there are other reasonable
options available. It is used like so:
function getindex(a::Vector, r::UnitRange)::Except{BoundsError}
[a[i]!? for i in r]
end
The !? operator only forwards exceptions declared of the appropriate subtype
bar()::Except{ErrorException} = error("Hello")!?
function baz()::Except{BoundsError}
# Still becomes an ordinary exception, because !(ErrorException <: BoundsError)
bar()!?
end
Similarly, failure to use the !? operator turns something into an ordinary exception
function baz2()::Except{ErrorException}
baz() # No !?, becomes ordinary exception
error("I meant this one")!?
end
To provide syntax-efficient handling of declared exceptions, we provide a new control-flow
concept, called match?. It is used as follows:
function get(d::Dict, val, default)
return match? d[val]
::BoundsError -> default
end
end
This would go along with introducing ordinary match statement on values
(see e.g. JuliaLang/julia#18285). I have a design for this
in mind, but it's somewhat beyond the scope of this proposal.
In particular, however, it is possible to perform value-level matching as well:
function read_passwds()
return match? open("/etc/passwd")
IOError(UV_ENOENT) -> ""
# All other IOErrors (e.g. EPERM) turned into ordinary exceptions
end
end
Failure to match turns the declared exception into an explicit one. To propagate, an
explicit !? is allowed on the end of the expression:
function read_passwds2()::Except{IOError}
return match? open("/etc/passwd")
IOError(UV_ENOENT) -> ""
# All other IOErrors propagated
end!?
end
Any is permissable as an exception declaration for higher-order functions:
function map(f, arr)::Except{Any}
[f(a)!? for a in arr]
end
If not annotated, a !? anywhere within the function defaults to Except{Any}.
# Equivalent to above
map(f, arr) = [f(a)!? for a in arr]
We have kind of seen this above already, but it's not a special case
error(str)::Except{ErrorException} = throw(ErrorException(str))!?
For ::Except annotated function (explicitly or implicitly), the implementation gets split:
foo(b::Bool)::Except{Int} =
if b
throw(1)!?
else
return 2
end
# Lowered to
function foo_inner(b)::ExceptOr
if b
return ExceptOrError(1)
else
return ExceptOr(2)
end
end
foo(b) = unwrap(foo_inner(b))
The !? and match? constructs would look up the inner function directly (similar to keyword
arguments) and propagate appropriately.