Lyricdev
GitHub

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

  1. Error of std.core is the root of everything thrown, caught and declared: interface Error { fn message(): string; fn cause(): ?Error { return null; } } (05 E6 O1). cause names the error this one wraps; the wrapping is the type's own field (O2).
  2. A value is throwable when its type conforms to Error (E3): a struct, a class or an enum that conforms, an interface value of Error or 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).
  3. throw e throws e; it is a statement, or an expression of type never wherever an expression may stand (x ?? throw NotFound { }). e is throwable (LYR-SEM0030).
  4. Exception of std.core is the ready-made error for a message: throw Exception("…"), and Exception("…", cause: e) for the error it wraps — what its cause() gives (12 E06; the review's M5-2). The call is its new (08 §1.2 rule 4); the fields behind it are text and inner, not message and cause: a type's members share one name space, and those two are Error's methods.

2. The throws set

  1. A function that may throw declares it after its return type: throws E for 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).
  2. The bare throws is throws Error: anything may be thrown, and the caller learns what by testing the value's type. The list is the form to prefer.
  3. The set is declared, never inferred, for a named function (K3). A function without a clause throws nothing.
  4. 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-SEM0042 otherwise.
  5. fn main() may declare throws (08 D18), like any function; its return type is int or void, with or without args: string[] (LYR-SEM0021).
  6. 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

  1. Every call of a throwing function is marked: it stands under try — the prefix try e, try? e or try! e (§6), or the body of a try { … } 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: a try around the lambda does not mark what its body calls.
  2. try covers everything to its right: try a + b is try (a + b) and marks every call in it (08 Y4). At the start of an expression — a binding's value, an argument, a return, 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 — in a() + try b() * c() the calls b() and c() are marked and a() is not, and a throwing call left of the mark is refused as any unmarked one is (LYR-SEM0138). try?, try! and a try with 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. A try that marks nothing is warned about (LYR-SEM0139): no error reaches it — nothing it covers throws, or a try inside it takes all of it — or what it covers is marked already, by a try block or a try around it or by a mark to its left in the same expression.
  3. Every type a site may throw — a marked call, or a throw — is covered (K8): by a try? or try! around the site, by a catch clause of a try around it (§4, §6), or by the throws set of the function the site stands in (LYR-SEM0034). A global's initializer, a constant's (a static let's) and a default cover nothing.
  4. An element covers a thrown type when it is that type — on the instance: Box<int> does not cover Box<string> (K5) — or when it is an interface the type conforms to. Error covers everything thrown.
  5. A call throws its function's set in the instance's terms: a method of Box<int> declared throws Wrong<T> throws Wrong<int>.

4. The try block and its clauses

  1. 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 the try. A block without a clause is refused (LYR-SEM0036).
  2. 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) — and e is an Error that carries the set (rule 6); a set of one is the typed clause, catch (e in A) binds an A. catch (e) and catch (_) take everything that reaches them; e is the one type that does, where it is one, and an Error otherwise (rule 6). A clause without a type stands last (LYR-SEM0035). A clause's types conform to Error (LYR-SEM0030).
  3. When no clause covers the value, it goes on as though the try were not there: to the clauses of a try around this one, or out of the function (§5).
  4. A clause is not inside its own try: what it throws goes past its sister clauses to the next try around, or out of the function (E9 C6). throw e in a clause throws the value it took; where e is 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 other throw — 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 an Error[] 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 ?Backtrace of std.core, where it was first thrown — kept in the debug profile, and null in the release profile, which keeps none. A Backtrace is a Display whose text is the frames in a panic's trace form, a line at <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.
  5. 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) above catch (e: Disk), or Error above anything. Within a set the same holds from left to right. Below the clause without a type, rule 2's LYR-SEM0035 is the one error.
  6. 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 e throws exactly that set (precise rethrow): catch (_: NotFound) { … } catch (e) { throw e; } in a function that declares throws Parse is covered when the body throws [NotFound, Parse]. A match (e) whose type patterns cover the set needs no default (LYR-SEM0050 names a missing type). The set is the binding's alone: stored elsewhere, the value is an Error. 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 an Error again. 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).
  7. (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

  1. 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).
  2. 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 enclosing try takes it (E7).
  3. An error that leaves main ends the program: error: <message> on the error stream, then caused by: <message> for each cause() in the chain and suppressed: <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.
  4. A defer body runs where its defer was registered: under the trys around that scope, not those inside it. A defer registered outside a try and run by a return inside it is not that try's to catch; one registered in a try's body is. What the body throws is a site of that scope (§3).
  5. A defer that throws on a normal exit — the end of its scope, a return, a break, a continue — ends that exit: the error leaves like any other, and the defers 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 remaining defers run (05 E7). Of two that throw on a normal exit, the first that ran wins.

6. The expression forms

  1. try? e is worth ?T for an e of type T: the value, or null when something under it throws — the error is dropped. It is not flattened: for an e of type ?int it is ??int, so "the call failed" stays apart from "the call gave null" (05 E4). Over an expression without a value try? stands as a statement, where it only drops the error; anywhere else it is refused (LYR-SEM0140).
  2. try! e is worth e'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 no defer runs past it (05 E8).
  3. try? and try! 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).
  4. try e catch (x: A) v catch (y) w is the expression form of the block (05 E4): its clauses are asked as a block's are (§4), and the expression is worth e'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 a match, 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 own try. throw as a clause's body throws with context — try read(p) catch (e: IoError) throw ConfigError { inner = e }.
  5. A clause belongs to the nearest try on its left (08 Y4): in try f() catch (_: A) try g() catch (_: B) 0 the clause for B is g's, and parentheses give it to the outer try. try? and try! take no clause (LYR-PAR0051).
  6. (Informative.) Each form opens a dispatch of its own, as the block does: try? clears the error and gives null there, 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

  1. using let f = e; binds f like a let and closes it — calls f.close() — when its scope is left, on every way out but a panic, in the scope's one LIFO list with its defers (05 E7 R2): using let a = …; defer d; using let b = …; closes b, runs d, closes a. using var, and a using without one name and a value, are refused (LYR-PAR0052). using is a word only before let or var.
  2. What using binds is a Closeable of std.core, interface Closeable { fn close(): void throws Error; } (LYR-SEM0143, R6). The close is a site of the scope, using its mark: what close() 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 (no LYR-SEM0139).
  3. An error from close() is a defer'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.
  4. A Closeable nothing closes is warned about (LYR-SEM0144, R3): a call's result an expression statement drops, or a plain let used only to call its other methods — not closed, not using-bound, not stored, returned or passed on.
  5. (Informative.) A use after the close is a run-time error once the library's types track it (R5); a for loop closes a Closeable iterator (R7) with the iterators.

8. Function values that throw

  1. 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 bare throws for Error (03 T17). Without it the type throws nothing. The set is part of the type, as a set: throws [A, B] and throws [B, A] are one type, fn() -> int and fn() -> int throws A two. Where a function type is itself a return type — of a function, of a lambda, of another function type — a throws behind 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 E throws. After one type a comma ends the function type, as in a parameter list; several are written in brackets.
  2. 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).
  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) -> int where fn(int) -> int throws E is expected, throws A where throws [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).
  4. A throwing function or method named as a value has its set in its type, in the instance's terms (§3 rule 5): check, declared fn check(n: int): int throws Negative, is a fn(int) -> int throws Negative, and limit.take bound to its object likewise (03 §8.4).
  5. 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 every try inside it. The function around the lambda covers nothing of it: the lambda runs later.
  6. A type parameter may stand as the set, throws E with E :: [Error] (§1 rule 2, LYR-SEM0030), and is inferred at a call from the argument's set (05 E2 K4): a set of none binds never, one type binds it, several bind Error (the K7 join) — fn map<T, U, E :: [Error]>(xs: T[], f: fn(T) -> U throws E): U[] throws E throws what f throws. 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. An E bound to never names nothing: a call whose set comes out empty throws nothing, needs no try, and a try over it is warned about (LYR-SEM0139).
  7. (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 E is never is compiled as a function that does not throw.

9. Panics and never

  1. 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 defer and no using close 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 of std.core, in scope without an import: panic(message): never (LYR-RT0008), which says the message as it is written; assert(condition) { message }, which panics with LYR-RT0011 when 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); assert is inline, so the check and the message stand at the call; unreachable(message = ""): never (LYR-RT0012); and todo(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.
  2. never is the type of what gives no value (05 E12): a throw, a call of a function that returns never — panic, unreachable, todo among them — a loop no break leaves (07 §1), an if or match whose 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();, and ok || fail("why") is a bool. Where it is reached, what holds it goes no further; and it ends the path it stands on and no other: in if (c) wrap(todo()) else 5 the branch is a call, typed as one, and its argument ends that branch alone — with c false the expression is 5. So for an arm and its guard, the right of ??, && and || and of their assignments, the call behind ?., and the operand of a try with clauses.
  3. never is 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); ?never and never[] are refused in a return position too. As a type argument for a parameter whose bound is Error it is the empty thrown set, as E = never is what a set of nothing binds (design 05 E2 K4, "Review 2026-10-09" S5): MapIter<SliceIter<int>, int, never>, f<never>() for f<E :: [Error]>, a struct literal's and a type path's arguments alike. So too as a type parameter's default under Error (M8c S20): the adapters' E :: [Error] = never, so MapIter<SliceIter<int>, int> is MapIter<SliceIter<int>, int, never>. try? over an expression of type never would be worth ?never and is refused the same way.
  4. A function that returns never does not return: every path ends in a throw, a panic, a call that does not return or a loop no break leaves (LYR-SEM0017 otherwise), and a return in it is refused unless what it returns does not return either (LYR-SEM0146).