ADR-210: The Joined Failures Are a Diagnostic, Not a Value — No Type for a List of Errors

Status: Superseded by ADR-280 — its D1-D3 are D21-D23 there Date: September 25, 2026 Target Version: Nikaia 0.0.186 Answers: open-decisions.md’s does the language have a type for a list of errors?, decided by the owner — option 3, and against that page’s recommendation of 1 Revises: ADR-115 D4’s example; ADR-170 §4’s second bullet is answered Corrected in part by: ADR-211 — D3’s the success path never sees an envelope: a Result is as large as its failure half whether it fails or not, and the fields cost 88 bytes on every call until they became one word Related: ADR-050 (overlap), ADR-078 §4 (no dyn), ADR-159 D2 (the bare channel)

1. Context

When the branches of an overlap fail together, or a cleanup fails while an error is already on its way, two failures exist at once and a throw can carry one. ADR-115 keeps the others: they are attached to the one that leaves as its secondary list, and ADR-170 built that — the list is on the envelope, survives throw error, and an uncaught failure prints it indented under the first.

ADR-115 D4 went one step further and wrote a handler that reads the list as a value: throw LoadFailed(error, error.secondary). That needs a type a program can write for a list of errors, and the question was which: an opaque Failures, dyn as a type, or neither.

2. The question behind the question

Asked why the list exists at all, the answer is one sentence: the language makes two failures happen at once, so it may not silently drop one. That purpose is the operator’s — an outage that takes the database and the cache down must say both in the log — and it is built.

Asked who needs to read the list in code, nothing in this repository does, and no program has been written that would. The one place it appeared is the record’s own example. A type introduced for an example is the shape this project refuses elsewhere: a construct whose only reader is the sentence that introduced it.

3. Decisions

D1 — The secondary list is a diagnostic. A program does not read it.

It travels with the failure, is kept by throw error, and is shown by the long form, an uncaught failure and nikaia explain. There is no error.secondary in the language, and no type for a list of errors.

D2 — ADR-115 D4’s example is withdrawn.

Combining failures is still a catch on the block, and the handler has the winner. A handler that re-throws it with throw error hands the whole list on; one that wraps it in an error of its own starts a new failure whose own site is the throw, and the joined failures stay with the log line of the one it caught.

D3 — Why every envelope has the field, though only two things fill it

Only an overlap and a failing cleanup fill the list. It still sits on every envelope (Thrown<E>, Raised) because a failure travels: one raised in a function without an overlap can reach, through ?, a function with one and become the failure the others join. Two envelope types — with and without a list — would need a conversion at every boundary between them. An empty list allocates nothing and is paid only on the failure path; the success path never sees an envelope. A bare failure from a library (ADR-159 D2) has no envelope and no list, and gains one only where a body joins (ADR-170 D1).

4. What either direction costs if it is wrong

If a program turns up that must read the list, an opaque Failures in std is still the answer — count, walk, print — and adding it then breaks nothing, because no program can have been written against a name that does not exist. The direction taken is the cheap one to reverse.

5. What is built

Nothing new. Part I 7.1 and 8.1.2 are corrected: they promised a readable error.secondary, which was never built. open-decisions.md loses its last open question, and #199 the bullet that pointed at it.

This site is open source. Improve this page.