6. Errors
A function that can fail says so in its signature, a call of it is marked where it stands, and what it throws is caught or passed on by a function that says so in turn — two channels, the value and the error, each in plain sight (05 E1–E12, 01 L5, 08 D9, D18, Y4, Y5).
Scope
What is thrown, the throws set, the try mark and what covers a thrown type (S1a); the try
block with its clauses and the error path at run time — propagation, defer on the way out,
main's report (S1b); the expression forms try?, try! and try … catch (S2a); a clause over
several types, the set a binding carries and the refusal of a clause no error can reach (S2b); a
defer that throws (S3a); using and Exception (S3b); where an error was thrown, and a rethrow
that keeps it (S3c); the thrown set of a function type (S4a); panics and never (S4b). Written
with M5; what a clause's binding tells of the error it took (§4 rule 4) with M8c. What is not
written here yet is decided in the design documents, not here.
Conformance. conformance/cases/06-errors/.
1. What is thrown
Errorofstd.coreis the root of everything thrown, caught and declared:interface Error { fn message(): string; fn cause(): ?Error { return null; } }(05 E6 O1).causenames the error this one wraps; the wrapping is the type's own field (O2).- A value is throwable when its type conforms to
Error(E3): a struct, a class or an enum that conforms, an interface value ofErroror of an interface extending it, a type parameter constrained to it. An enum is the natural form —enum ParseError :: [Error] { Empty, BadDigit(char, int); … }. Anything else — a number, a string, an array, an optional — is not (LYR-SEM0030). throw ethrowse; it is a statement, or an expression of typeneverwherever an expression may stand (x ?? throw NotFound { }).eis throwable (LYR-SEM0030).Exceptionofstd.coreis the ready-made error for a message:throw Exception("…"), andException("…", cause: e)for the error it wraps — what itscause()gives (12 E06; the review's M5-2). The call is itsnew(08 §1.2 rule 4); the fields behind it aretextandinner, notmessageandcause: a type's members share one name space, and those two areError's methods.
2. The throws set
- A function that may throw declares it after its return type:
throws Efor one type,throws [A, B]for several — a set, in brackets from two on like every list (08 D5/D6); two without the brackets are refused (LYR-PAR0049). The order means nothing; a type named twice is refused (LYR-SEM0137). Every element is throwable (LYR-SEM0030). - The bare
throwsisthrows Error: anything may be thrown, and the caller learns what by testing the value's type. The list is the form to prefer. - The set is declared, never inferred, for a named function (K3). A function without a clause throws nothing.
- An implementation of an interface member throws at most what the member declares: every
type of its set is covered by the member's (§3 rule 4, K6) —
LYR-SEM0042otherwise. fn main()may declarethrows(08 D18), like any function; its return type isintorvoid, with or withoutargs: string[](LYR-SEM0021).- A function type has a set too,
fn(string) -> int throws ParseError: a function value and a lambda throw what their type says (§8).
3. The try mark and coverage
- Every call of a throwing function is marked: it stands under
try— the prefixtry e,try? eortry! e(§6), or the body of atry { … }block — of its own function (LYR-SEM0138). A refactoring that makes a function throw shows at every caller instead of passing through unseen (E1). A lambda's body is a function of its own: atryaround the lambda does not mark what its body calls. trycovers everything to its right:try a + bistry (a + b)and marks every call in it (08 Y4). At the start of an expression — a binding's value, an argument, areturn, the right of an=, what stands in parentheses, a branch — it covers all of it. The plain mark may also stand to the right of an operator: there it covers from where it stands to the end of that expression and changes nothing about how the expression is grouped — ina() + try b() * c()the callsb()andc()are marked anda()is not, and a throwing call left of the mark is refused as any unmarked one is (LYR-SEM0138).try?,try!and atrywith clauses are worth something else than what they cover, so they stand at its start, and to the right of an operator they are refused (LYR-PAR0050). The value and the type are the expression's. Atrythat marks nothing is warned about (LYR-SEM0139): no error reaches it — nothing it covers throws, or atryinside it takes all of it — or what it covers is marked already, by atryblock or atryaround it or by a mark to its left in the same expression.- Every type a site may throw — a marked call, or a
throw— is covered (K8): by atry?ortry!around the site, by acatchclause of atryaround it (§4, §6), or by thethrowsset of the function the site stands in (LYR-SEM0034). A global's initializer, a constant's (astatic let's) and a default cover nothing. - An element covers a thrown type when it is that type — on the instance:
Box<int>does not coverBox<string>(K5) — or when it is an interface the type conforms to.Errorcovers everything thrown. - A call throws its function's set in the instance's terms: a method of
Box<int>declaredthrows Wrong<T>throwsWrong<int>.
4. The try block and its clauses
try { … } catch (e: T) { … } catch (e) { … }runs the body; when something in it throws, the clauses are asked in order, and the first whose type covers the thrown value's type (§3 rule 4) takes it: the error is handled, its clause runs, and execution continues after thetry. A block without a clause is refused (LYR-SEM0036).- A typed clause binds the value under its own type — a class's object, a struct's or an
enum's value, an interface value of the clause's interface — so its fields and its variants
are there to read and match. The set form
catch (e in [A, B])takes a value of any of its types — one alone without the brackets,catch (e in A), several in them, the list rule (LYR-PAR0049) — andeis anErrorthat carries the set (rule 6); a set of one is the typed clause,catch (e in A)binds anA.catch (e)andcatch (_)take everything that reaches them;eis the one type that does, where it is one, and anErrorotherwise (rule 6). A clause without a type stands last (LYR-SEM0035). A clause's types conform toError(LYR-SEM0030). - When no clause covers the value, it goes on as though the
trywere not there: to the clauses of atryaround this one, or out of the function (§5). - A clause is not inside its own
try: what it throws goes past its sister clauses to the nexttryaround, or out of the function (E9 C6).throw ein a clause throws the value it took; whereeis the clause's own binding, thrown in the clause itself, it is the same error going on (05 E6 O3): the errors suppressed into it stay with it (§5 rule 5), and so, in the debug profile, does where it was first thrown (§5 rule 3). Any otherthrow— the value under another name, or from a lambda — starts an error of its own. The binding also tells of that record (05 E6 O3, the review's M5-3):e.suppressed()is anError[]of the errors suppressed into it, in the order they were (§5 rule 5, §7 rule 3), a new array each call;e.backtrace()is a?Backtraceofstd.core, where it was first thrown — kept in the debug profile, andnullin the release profile, which keeps none. ABacktraceis aDisplaywhose text is the frames in a panic's trace form, a lineat <function> (<file>:<line>)each, innermost first. Both are asked of the binding itself, in its clause: an error stored, passed on or captured by a lambda is a value like any other and has neither (LYR-SEM0012). Where the binding's type has a member of either name, the name is that member. - A type is caught once per
try(LYR-SEM0141, E9 C1), and a clause no value can reach is refused (LYR-SEM0142, C2): a type of it is covered (§3 rule 4) by a type caught before it —catch (e: IoError)abovecatch (e: Disk), orErrorabove anything. Within a set the same holds from left to right. Below the clause without a type, rule 2'sLYR-SEM0035is the one error. - The binding of a set clause, or of a clause without a type, carries a set (05 E2 K7): the
set's types; for the clause without a type, what reaches it — every type the body throws past
the
trys inside it, less the types the clauses above cover.throw ethrows exactly that set (precise rethrow):catch (_: NotFound) { … } catch (e) { throw e; }in a function that declaresthrows Parseis covered when the body throws[NotFound, Parse]. Amatch (e)whose type patterns cover the set needs no default (LYR-SEM0050names a missing type). The set is the binding's alone: stored elsewhere, the value is anError. Where the set is one type, the binding is that type (the review's M5-6): its fields and its variants are there, as in a typed clause, and it is an error wherever one is asked. A second type the body comes to throw makes it anErroragain. Where the set is empty although the body throws — the clauses above cover every type of it — the clause is dead, and warned about (LYR-SEM0171). - (Informative.) Since the value travels with its type, the clause test is one comparison of the value's type descriptor, an interface clause a search of the type's short conformance list.
5. The error path
- A throw site that is not caught where it stands leaves: the function returns to its caller at once, without a value, and the caller's marked call is now the site — caught there, or leaving in turn (E1). No frame is skipped; a throw costs what a return costs (01 L5).
- On the way out of every scope it leaves, an error runs the scope's
defers, the last registered first, innermost scope first — the same bodies a normal exit runs — before a clause of an enclosingtrytakes it (E7). - An error that leaves
mainends the program:error: <message>on the error stream, thencaused by: <message>for eachcause()in the chain andsuppressed: <message>for each error appended to it (rule 5), and the exit code 1 (05 E6 O4) — a panic's is 101. In the debug profile the report goes on with where the error was thrown, its frames in a panic's trace form (01 E8); the release profile keeps no trace of an error. - A
deferbody runs where itsdeferwas registered: under thetrys around that scope, not those inside it. Adeferregistered outside atryand run by areturninside it is not thattry's to catch; one registered in atry's body is. What the body throws is a site of that scope (§3). - A
deferthat throws on a normal exit — the end of its scope, areturn, abreak, acontinue— ends that exit: the error leaves like any other, and thedefers registered before it run all the same. One that throws while an error is in flight does not replace it: the first error wins, the second is appended to it as suppressed, and the remainingdefers run (05 E7). Of two that throw on a normal exit, the first that ran wins.
6. The expression forms
try? eis worth?Tfor aneof typeT: the value, ornullwhen something under it throws — the error is dropped. It is not flattened: for aneof type?intit is??int, so "the call failed" stays apart from "the call gavenull" (05 E4). Over an expression without a valuetry?stands as a statement, where it only drops the error; anywhere else it is refused (LYR-SEM0140).try! eis worthe's value; when something under it throws, the program ends with a panic —panic [LYR-RT0010]: 'try!' on an error: <message>, exit code 101 — and nodeferruns past it (05 E8).try?andtry!take every error under them themselves: they mark the calls they cover (§3 rule 1) and cover what those throw (rule 3). The sign is written against the keyword —try !done()is the mark over a negation — and the form stands at the start of what it covers, as the mark does (LYR-PAR0050).try e catch (x: A) v catch (y) wis the expression form of the block (05 E4): its clauses are asked as a block's are (§4), and the expression is worthe's value, or the value of the clause that took the error. A clause's body is an expression, or a value block{ …; v }; a value block without a tail leaves on every path —return,throw,break,continue— unless the expression has no value (LYR-SEM0033). The parts unify like the arms of amatch, or each meets the context where there is one (LYR-SEM0016). The rules of §4 hold: the clause without a type last (LYR-SEM0035), a clause not inside its owntry.throwas a clause's body throws with context —try read(p) catch (e: IoError) throw ConfigError { inner = e }.- A clause belongs to the nearest
tryon its left (08 Y4): intry f() catch (_: A) try g() catch (_: B) 0the clause forBisg's, and parentheses give it to the outertry.try?andtry!take no clause (LYR-PAR0051). - (Informative.) Each form opens a dispatch of its own, as the block does:
try?clears the error and givesnullthere,try!panics there, the clauses are tested there. Nothing is unwound; the cost is the block's (§4 rule 7, §5 rule 1).
7. Closing: using
using let f = e;bindsflike aletand closes it — callsf.close()— when its scope is left, on every way out but a panic, in the scope's one LIFO list with itsdefers (05 E7 R2):using let a = …; defer d; using let b = …;closesb, runsd, closesa.using var, and ausingwithout one name and a value, are refused (LYR-PAR0052).usingis a word only beforeletorvar.- What
usingbinds is aCloseableofstd.core,interface Closeable { fn close(): void throws Error; }(LYR-SEM0143, R6). The close is a site of the scope,usingits mark: whatclose()throws on the bound type is covered as any site is (§3) — and a close that cannot throw needs no mark and draws no warning (noLYR-SEM0139). - An error from
close()is adefer's error (§5 rule 5, R4): on a normal exit it leaves, while an error is in flight it is appended to it as suppressed. - A
Closeablenothing closes is warned about (LYR-SEM0144, R3): a call's result an expression statement drops, or a plainletused only to call its other methods — not closed, notusing-bound, not stored, returned or passed on. - (Informative.) A use after the close is a run-time error once the library's types track it
(R5); a
forloop closes aCloseableiterator (R7) with the iterators.
8. Function values that throw
- A function type may say what a call of it throws, after its return type and under the
rules of a function's clause (§2):
fn(string) -> int throws ParseError,fn() -> void throws [IoError, ParseError], the barethrowsforError(03 T17). Without it the type throws nothing. The set is part of the type, as a set:throws [A, B]andthrows [B, A]are one type,fn() -> intandfn() -> int throws Atwo. Where a function type is itself a return type — of a function, of a lambda, of another function type — athrowsbehind it could be its own or that of the one that returns it, and without parentheses it is refused (LYR-PAR0056):fn make(): (fn() -> int throws E)returns a throwing function and throws nothing itself,fn make(): (fn() -> int) throws Ethrows. After one type a comma ends the function type, as in a parameter list; several are written in brackets. - A call through a value of such a type is a site like the call of a throwing function: it is marked (§3 rule 1), and its set is covered (rule 3).
- A function value coerces to a function type that throws more: the same parameters and
return, and every type of its set covered by the target's (05 E2 K6) —
fn(int) -> intwherefn(int) -> int throws Eis expected,throws Awherethrows [A, B]is. Never the other way: a value that may throw does not fit a type that throws less (LYR-SEM0001). The parameters and the return still match exactly (03 §8.1). - A throwing function or method named as a value has its set in its type, in the instance's
terms (§3 rule 5):
check, declaredfn check(n: int): int throws Negative, is afn(int) -> int throws Negative, andlimit.takebound to its object likewise (03 §8.4). - A lambda throws what its type says (05 E2 K3). The parenthesized form may write the set
after its return type,
(s: string): int throws ParseError => try parse(s), and several types in brackets (LYR-PAR0049); otherwise a position that expects a function type gives the set, and the body is held to it (LYR-SEM0034); where neither fixes it, the set is what the body lets escape — the types its sites throw past everytryinside it. The function around the lambda covers nothing of it: the lambda runs later. - A type parameter may stand as the set,
throws EwithE :: [Error](§1 rule 2,LYR-SEM0030), and is inferred at a call from the argument's set (05 E2 K4): a set of none bindsnever, one type binds it, several bindError(the K7 join) —fn map<T, U, E :: [Error]>(xs: T[], f: fn(T) -> U throws E): U[] throws Ethrows whatfthrows. Where several arguments could bind it, one binding stands, as for any type parameter (03 §9.2), and an argument that throws beyond it is refused there. AnEbound tonevernames nothing: a call whose set comes out empty throws nothing, needs notry, and atryover it is warned about (LYR-SEM0139). - (Informative.) A function value is two words, code and environment
(03 §8.1); the code takes its caller's error slot whether its type
throws or not, so the coercion of rule 3 changes nothing at run time and wraps nothing, and code
that cannot throw never writes the slot. An instance whose
Eisneveris compiled as a function that does not throw.
9. Panics and never
- A panic is no error: nothing throws it and no clause takes it. It ends the program with
its report and the exit code 101, and no
deferand nousingclose runs past it (05 E8) — the report and the codes are the runtime's (13 §1.4). Besides the runtime's checks, a program panics through four functions ofstd.core, in scope without an import:panic(message): never(LYR-RT0008), which says the message as it is written;assert(condition) { message }, which panics withLYR-RT0011when the condition is false — the message a lambda,assert(n > 2) { f"n = {n}" }, built only then (design 10 "Review 2026-10-12" D1, Kotlin's form; a string in its place is a type mismatch,LYR-SEM0001);assertisinline, so the check and the message stand at the call;unreachable(message = ""): never(LYR-RT0012); andtodo(message = ""): never(LYR-RT0013). The last three say their word first (the review's M5-14) —assertion failed,unreachable,not implemented— and the message behind it where one is given:not implemented: the parser. An assertion names its condition as it is written between the two:assertion failed: n > 2: n is small. neveris the type of what gives no value (05 E12): athrow, a call of a function that returnsnever—panic,unreachable,todoamong them — aloopnobreakleaves (07 §1), aniformatchwhose every branch is one, and an operator over one alone. It fits every type where a value is wanted, an operand included:let n: int = found ?? unreachable();, andok || fail("why")is abool. Where it is reached, what holds it goes no further; and it ends the path it stands on and no other: inif (c) wrap(todo()) else 5the branch is a call, typed as one, and its argument ends that branch alone — withcfalse the expression is5. So for an arm and its guard, the right of??,&&and||and of their assignments, the call behind?., and the operand of atrywith clauses.neveris written only as a return type: the whole return type of a function, a lambda or a function type,fn fail(m: string): never. Anywhere else — a parameter, a binding, a field, an element, an optional, a type argument for any other parameter, a thrown set, an alias — it would type a value that cannot exist and is refused (LYR-SEM0145);?neverandnever[]are refused in a return position too. As a type argument for a parameter whose bound isErrorit is the empty thrown set, asE = neveris what a set of nothing binds (design 05 E2 K4, "Review 2026-10-09" S5):MapIter<SliceIter<int>, int, never>,f<never>()forf<E :: [Error]>, a struct literal's and a type path's arguments alike. So too as a type parameter's default underError(M8c S20): the adapters'E :: [Error] = never, soMapIter<SliceIter<int>, int>isMapIter<SliceIter<int>, int, never>.try?over an expression of typeneverwould be worth?neverand is refused the same way.- A function that returns
neverdoes not return: every path ends in athrow, a panic, a call that does not return or aloopnobreakleaves (LYR-SEM0017otherwise), and areturnin it is refused unless what it returns does not return either (LYR-SEM0146).