ADR-115: An overlap Keeps Every Failure — the First in Written Order Wins, the Rest Are Attached as secondary

Status: Superseded by ADR-292 — its D1-D4 are D8-D11 there (D1 as narrowed and D4’s example as withdrawn by ADR-210) Date: September 16, 2026 Target Version: Nikaia 0.0.8 Related: ADR-050 D2 and D5 (overlap waits for all its branches; a failing branch fails the block, first in written order), ADR-006 D4 (a cleanup error while another is unwinding is attached, not substituted), ADR-023 D6 (what an error carries without anyone attaching it), Part I 7.1 and 8.1.2

1. Context

An overlap { … } runs its branches together and waits for all of them. Where more than one branch fails, the block fails with the error of the first in written order — the only order the source has, so the only one that makes the outcome reproducible. What the specification did not say was what becomes of the others, and the answer by omission was: nothing. Three loads, two of them failing on the same outage, and the log shows one.

The language already has the shape for a second error that arrives while a first one is live. A cleanup that fails while an error is unwinding does not replace the error, it is attached to it as a secondary. What the specification had not said, there either, is how a program sees what was attached. This record says both, once, for both cases.

2. Decisions

D1 — An error carries a list, secondary, and it is empty until something joins.

Every error value has secondary: a list of further errors, in the order they joined, each with its own raise site as ADR-023 D6 gives every error. It costs nothing on the path where nothing fails, and on the path where something does it is what the log and nikaia explain print, indented under the primary.

A catch still catches one error and chooses by its type, unchanged. The list is for a handler and a log to see everything; it is not a second thing for catch to match on.

D2 — In an overlap, the failures of the other branches are the winner’s secondary, in written order.

The block still waits for every branch (ADR-050 D2), so when it ends every outcome is known. The first failing branch in written order is the block’s error; every later failing branch is appended to its secondary in written order. Running to completion is what makes this list a fact and not a race: were the other branches cancelled at the first failure, which of them had already failed would depend on the clock.

A branch that hangs holds the block, as it does today. A timeout is the branch’s own, written as a select against a deadline inside it; the block takes none.

D3 — A cleanup error attaches the same way, and the result is a tree.

ADR-006 D4’s secondary is this list. A cleanup that fails while a branch is already failing joins that branch’s error; the branch’s error, with its own secondary, then joins the winner’s. A printout indents twice. Nothing about cleanup changes; what is new is that its attachment and overlap’s are one mechanism.

D4 — A branch handles its own failure; the block is where failures are combined.

Revised by ADR-210: the handler below reads error.secondary as a value, and that is withdrawn — the list is a diagnostic that travels with the failure and throw error hands on, not a value a program reads. The rest of D4 stands.

A catch inside a branch is unchanged: a handler that yields a value makes the branch succeed, and it appears nowhere as a failure; a handler that throws makes the branch fail with what it threw, which takes part in D2 like any other failure. What a branch cannot do is see the other branches’ failures, because branches meet on nothing (ADR-050 D3) and run at once. Combining is done on the block:

let (user, rights, prefs) = overlap {
    db::load_user(id)
    db::load_rights(id) catch { throw RightsUnavailable(error) }
    cache::load_prefs(id)
} catch {
    throw LoadFailed(error, error.secondary)
}

3. Consequences

4. What this does not decide

5. What is built

Since this was written: steps 1, 2 and 4 are ADR-170’s, and step 3 is ADR-239 D3’s — a function’s settle point joins a cleanup’s failure to the error already leaving (0.0.224), and an overlap branch settles its own and joins its error (0.0.225). What is left is #199. The text below is the day this was written.

Nothing. The block’s join keeps the first failure in written order and drops the rest; a cleanup error while unwinding is attached below in the language below’s own way, with nothing a program can read. The order of work:

  1. secondary on the error carrier, and the printer showing it indented, for nikaia explain and the uncaught-error path;
  2. the overlap join appending the later failures in written order;
  3. the cleanup attachment through the same list;
  4. Part I 8.1.2’s example, and a test with two failing branches.

#199 carries it.

This site is open source. Improve this page.