Laurel Language Designer Guide

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

x.f

if x is null then throw

Index out of bounds

a[i]

if i < 0 or i >= a.length then throw

Division by zero

a / b

if b == 0 then throw

Bad cast

(Sub) x

if !(x is Sub) then throw

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.