Status: Accepted
Date: October 14, 2026
Answers: what a call through a function value can fail with, when the value
was stored, handed on or kept rather than called at once
Related:
ADR-023 (throws names no types; the set is inferred into the ledger);
ADR-280 D15 (a set of two or more is one generated sum);
ADR-288 D31 (sync(f): a property the lambda decides);
ADR-345 D2 (a fatal error is taken only by name);
ADR-346 D5, D6, D7 (a task’s errors reach its joiner; detached errors; supervisors);
Part I 5.4 C, 7.1; Part II 12.8; Part III 13.5, 17.1
What can job.run() fail with, where run is a field holding a function?
throws names no types: what a function fails with is inferred from its body
over the whole program (Part I 7.1), and a catch sees the sum of what can
arrive. A function value handed to a parameter that only calls it counts with
that set for the callee. One that is kept, in a field, a list, a
supervisor::Child or a server’s handler, has so far followed its type alone,
fn() throws: can fail with something. The set the compiler inferred was
not carried with the value, so a catch over a call through it cannot be
exhaustive, a fatal error in it cannot be seen before the program runs, and a
supervisor hands a child’s error on wrapped (ADR-346 D7) because the child’s
set is unknown.
All rows are from memory, not checked against a current manual.
| Language | A function value’s errors | A stored function |
|---|---|---|
| Java | checked exceptions written by hand; java.util.function declares none |
lambdas that throw are wrapped in unchecked exceptions; a generic throws E carries one type |
| Swift | rethrows for a parameter called at once; typed throws throws(E) is generic |
throws(E) is part of the closure’s type and stays with it |
| Rust | the error is in the result type, Fn() -> Result<T, E> |
the type keeps E; mixed closures in one list need one E |
| Koka | effect rows, inferred and polymorphic | a stored function keeps its row; a list of them has the union |
| Kotlin, C# | unchecked; nothing in the type | nothing to keep |
Java’s failure is the cost of writing the sets by hand. Where the sets are inferred (Koka) or generic (Swift, Rust), a stored function keeps its errors.
| Option | Why it lost, or won |
|---|---|
A A kept function fails with something (?), as before |
no catch over it is exhaustive and no fatal error in it is visible to the compiler; the run time has to recover at the end what the compiler knew and dropped |
| B A function value carries its inferred set wherever it goes | chosen (D1-D7) |
C Only std’s types carry it, case by case |
two rules for one thing; a program’s own struct Job { run: fn() throws } stays unprovable, and each new std type that keeps a function repeats the work |
A named function carries the set inferred for its body; a lambda the set of
its body. The set travels with the value through a let, an assignment, a
field, an element of a list or a map, a result and a parameter, whether the
receiving function calls the value at once or keeps it. fn() throws in the
source still names no types; the set is the compiler’s, as for a function
(ADR-023).
A call through a function value can fail with exactly the set the value
carries, and a catch over it sees that set’s sum (Part I 7.1). Writing a
function value, storing it, handing it on or returning it calls nothing and
can fail with nothing.
Where values with different sets meet in one place, the place carries the
union of their sets: the arms of an if or a match, the elements of a list
or a map, the values assigned to one variable or field, the arguments handed
to one kept parameter by every caller. A code parameter the body does not keep
is from(p), replaced at each call by what that call hands it (D4). A place no
failing value reaches carries the empty set, and a call through it cannot fail.
Changed by ADR-error-sets-1 D1, D2.
A function whose body calls a function value it was given says so in its
throws as from(p), where p is the parameter or a field path of it
(from(handler), from(job.run)). A caller replaces from(p) with the set of
what it passed. A type that holds a function value carries that value’s set
as part of the type of each value of it, as TaskHandle[T] carries its
body’s (ADR-346 D5).
supervisor::run throws its children’s fatal errors themselves.run throws supervisor::Escalated and the fatal members of its children’s
sets. A child that ends with a fatal error is not restarted (ADR-346 D7); the
supervisor stops every child and throws that error, which a catch takes by
naming its type (ADR-345 D2). supervisor::Handed is withdrawn.
An error a net::serve handler ends with is the error of a detached task
(ADR-346 D6): printed in the long form, and a fatal one replaces the exit
status at main’s end. Which handler errors are fatal is known from the
handler’s set.
"?" stays for what no ledger describes.A set member "?" still means this compiler cannot name it: a call into
code no ledger describes. A function value is never a reason for it.
There is no global mutable data (ADR-281 D32) and no trait object (a trait is not a type, Part I), so code a function keeps is always reached from one of its parameters or its result: a registry is a value its caller holds.
struct Registry { tasks: Vec[Task] }
impl Registry {
fn register(ref mut self, t: Task) { self.tasks.push(t) } // code: self.tasks.run: from(t)
fn fire(ref self) throws { for t in self.tasks { t.run() } } // throws: from(self.tasks.run)
}
Shared, the two ends of a channel, the positions the ledger’s
sharing makes one allocation - carry one set: the union of everything put
there in the value’s whole life, wherever in the program it was put.code names, for each place reached from a parameter or
the result, what the function puts there, as maps does for maps:
code = ["self.tasks.run: from(t)", "<result>.run: Bad"]. A path is one
field long below the parameter, through the elements of a list or a map; a
deeper place joins its field’s. A caller adds the set on the right, with
from(p) replaced by what it passed, to the value it holds at that path.from(…) member, carry a type parameter for the
error in the Rust they lower to, filled with the generated sum of the place
(ADR-280 D15). Code is converted to that sum where it enters the place, and a
catch matches the sum. A box is left only for "?".error is what can arrive as the
compiler inferred it. A function value is a call that has not happened yet;
dropping its set where it is stored makes the rule true only for calls the
compiler sees at once, and makes every proof about a stored function’s
failures impossible.catch already builds over two calls; a list
of functions is the same question asked of one place.sync it already records that the lambda decides (ADR-288 D31);
from(p) is the same answer for throws. Carrying the set on the value’s
type is what ADR-346 D5 did for TaskHandle and keeps the two rules one.throw error in a handler
throws what the arm named.catch the arms for what is put in later and needs no flow
analysis. Rejected: places closed by the program package (no such place
exists); writing the set in the source, fn() throws(E) (§7, still not
needed); a box opened by a downcast at the catch (an allocation per error,
a test per arm, and a refusal by rustc wherever an opening is missed).with.from(p); a
change to what such a function’s argument can throw changes what its
callers see, and ADR-288’s warning on a weakened contract applies.supervisor::Handed
are not needed where the compiler knows the set.net::serve’s handler errors reach the exit status like a detached task’s.catch over a call through kept code that
names every type is complete; the set is the storage’s (D8).fn() throws(E)); the set stays inferred.