3.3. Exceptions
Every source language Laurel currently targets signals unrecoverable-here conditions by throwing,
and any sound reasoning about their programs has to account for it. Exceptions are therefore
first-class in Laurel — throws, throw, try / catch / finally, and the exceptional
contracts — rather than something each frontend encodes for itself. The alternative, letting every
frontend lower exceptions into its own mixture of flags and branches, multiplies the same work per
frontend and widens the surface where each of them can get it subtly wrong.
No root exception type. Laurel does not define a Throwable, and does not require a thrown value
to belong to any hierarchy: a procedure may declare throws int and throw 3. Java and Python each
have a root (Throwable, BaseException), but they are different roots with different tiering
rules, and JavaScript has none at all — throw 42 is legal there, and the convention of throwing an
Error is enforced by linters rather than by the language. Imposing one root would mean every
frontend adapting its own hierarchy to Laurel's, which is exactly the per-frontend work this feature
exists to avoid.
Subtype-aware typing of the handler binding instead. What a handler actually needs is a type for
its binding, so that reading a field of the caught value is well-typed. Laurel types a catch
binding at the least common ancestor of the exception types that can reach it — the types thrown in
the try body, and the declared throws types of the procedures it calls. Each frontend's own
hierarchy then does the work: where the reachable types share an ancestor, the handler sees it
directly and needs no downcast. Where they share nothing useful (unrelated types, or JavaScript's
arbitrary values), a frontend boxes the value on the way out and unboxes it in the handler, which
keeps that cost in the frontends that actually have the problem. The rejected alternative was to
type every binding at a synthesized universal root, which would put a downcast in front of every
field access in every handler, in every frontend.
Predicate dispatch rather than type dispatch. A catch clause carries an arbitrary boolean guard
over its binding, catch e when <condition on e>, with ordered first-match-wins clauses and an
unguarded clause as catch-all. Type-based dispatch is then one guard shape, catch e when e is T,
which covers Java's static catch (Type e) and Python's except T as e; JavaScript's hand-written
e instanceof Error && e.code == … is another. A type-keyed clause list would have needed a second,
separate mechanism to express the JavaScript case, so the general form is the cheaper one to support.
finally is native. All three languages have it, with the same core rule — the arm runs on normal
completion, on a caught exception, on an uncaught one, on a return or re-throw from a handler, and
nested arms chain outward — so Laurel provides it directly. Language-specific variants stay in the
frontends: Java's try-with-resources desugars to a finally that closes the resource, and Python's
else arm (which runs before finally only when the body completed normally) desugars into the
body.
Errors versus exceptions are not modelled. Java separates catchable Exception from fatal Error,
Python separates Exception from SystemExit, and the line differs per language. Laurel needs no
construct for it: a frontend expresses the tiering by which parent a type extends, and a catch-all
written as a guard over the catchable tier is simply false for a fatal type. The distinction falls
out of ordinary subtyping.
Declared throwing behavior, checked. A procedure that may throw says so with throws, and Laurel
enforces catch-or-declare during resolution: a procedure with no throws may not let an exception
escape, and one declaring throws T may only let subtypes of T escape. This is a direct fit for
Java's checked exceptions, and procedures from Python and JavaScript carry a throws too even though
those languages have no such surface construct. The gain is that failures which are silent at runtime
in the source language become statically visible: a frontend emits an explicit guarded throw where
the source language would have failed implicitly.
Implicit failure | Source | Explicit check the frontend emits |
|---|---|---|
Null dereference |
|
|
Index out of bounds |
|
|
Division by zero |
|
|
Bad cast |
|
|
Requiring a frontend to declare unchecked exceptions this way would make nearly every procedure throwing, which costs propagation and unwrapping at every call site. Inferring a procedure's throwable types from its body is the planned way out; see the Planned features section.
What this asks of a frontend. Map throwing signatures onto throws and the exceptional contracts,
and handlers onto try / catch with predicate guards; define the exception types and their
subtyping; handle any catchable-versus-fatal tiering; and desugar what Laurel does not model
natively, such as Python's else arm, try-with-resources, multi-catch lists, and throwing values with
no common ancestor. The contract clauses need surface syntax of the frontend's own — the Java
frontend, for example, exposes them as static helper calls in the method body — but the names are the
frontend's choice; what matters is that they map onto throwsOn behavior cases.