Skip to content

Instantly share code, notes, and snippets.

@Keno
Last active December 5, 2025 00:22
Show Gist options
  • Select an option

  • Save Keno/7d1fb27a003afba6de50686ccfbb6610 to your computer and use it in GitHub Desktop.

Select an option

Save Keno/7d1fb27a003afba6de50686ccfbb6610 to your computer and use it in GitHub Desktop.

Declared exceptions

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.

Declared exceptions

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.

Interaction with other code

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

Syntax for declared exception propagation

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

Inline handling of declared exceptions

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

Higher order functions and implicit Except

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]

Throwing declared exceptions

We have kind of seen this above already, but it's not a special case

error(str)::Except{ErrorException} = throw(ErrorException(str))!?

Implementation

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment