ADR-164: A Joining Block Has One Outcome, and a Handler on It Binds What the Branches Threw

Status: Superseded by ADR-292 — its D1-D3 are D18-D20 there Date: September 20, 2026 Target Version: Nikaia 0.0.82 Related: ADR-163 D3 (the defect this closes), ADR-115 D4 (the written example that did not lower, and whose secondary list this makes room for), ADR-050 D5 (the first failure in written order wins), ADR-148 D1 (select), ADR-157 D2 (the envelope is opened once at the binding), ADR-159 D2, ADR-160 D1 (the other two channels), Part III C.1, Part I 7.1 and 8.1.2

1. Context

ADR-163 D3 named a defect rather than fixing it: overlap { … } catch { … } does not lower. It is ADR-115 D4’s own written example — the form that record uses to say how a block combines what its branches failed with — and the reason to leave it there was that fixing it is that record’s own first step.

This is that step, and measuring it turned one defect into four. All four are Part III C.1 — rustc speaking about a file the author never wrote — and two of them need no joining block at all to reach.

2. Decisions

D1 — A joining block’s branches become one outcome, in std.

nikaia_std::task::combine<n> takes the branches’ results in written order and answers the first Err among them, which is ADR-050 D5 written as the language below’s own control flow. The block’s value is that one Result.

It was a ? per branch at the call before, and a ? leaves the function — so where a catch on the block was what handled the failure, the handler got a match over the vehicle’s tuple as though it were a Result. Where the failure does leave the function, the ? is now one, on the block.

One function that sees every outcome is the point beyond the fix. ADR-115 keeps the later failures as the winner’s secondary, and combine<n> is the only code that ever holds them all. Writing the combination at the call site and again inside that record would be writing the same loop twice.

A select’s arms are the same rule in the shape that construct has: where the handler is on the block, the winning arm takes its own failure apart and its value becomes the Ok half, because a ? there would leave the function too.

D2 — A handler on the block binds the set the branches throw.

The enclosing function may not be throws at all — overlap { … } catch { … } is the shape where it usually is not — so its channel is the box, and match error { LoadError::Missing(w) => … }, which is Part I 7.1’s whole point, does not compile over one.

A joining block therefore has an error set of its own: the union of its branches’, in the ledger’s own order. The channel that set travels in is the same derivation a function’s key gets — Thrown<E>, a library’s type bare, a generated sum, or the box — asked of the set instead of of the key. Both the Ok the branches are wrapped in and what the handler binds read it, so the two cannot disagree.

It resolves no more than was already resolved here (ADR-011 D2): a branch’s callee by the name the source wrote, and that callee’s throws column off a ledger — which is exactly what the catch already did to decide whether to open an envelope. A branch whose callee no ledger knows contributes the absence of a claim, which makes the set the box: the safe direction.

D3 — And rustc says nothing about the file it was handed.

Two warnings were found by compiling the shapes above, and neither needs a joining block to reach:

A warning about a generated file is C.1 one severity down, and it is held by a test that asserts rustc’s stderr is empty rather than that it succeeded.

3. Consequences

4. What this does not decide

5. What is built

All three decisions. crates/nikaia/tests/joining.rs is eleven tests, most of which compile and run the result: the handler on an overlap and on a select, the handler matching the branches’ variants with the envelope opened once, and one test that asserts rustc said nothing about either of the two programs that used to make it warn.

This site is open source. Improve this page.