A transfer service routes every failure through its result type, including programming bugs. What does that cost its callers?
answer
- not every failure earns the channel
- ask what the caller can decide
- a broken assumption is not an outcome
- sprawl reaches every signature in between
- a catch-all branch swallows defects
basics
~20 sCallers end up matching on failures they cannot act on, and the failure description swells into a catch-all no layer can decide about. The channel is for outcomes the operation anticipates, not for broken assumptions.
solid answer
~40 sA typed error channel earns its plumbing when the failure is an anticipated outcome the caller can make a decision about: an unsupported currency, an unknown destination account, insufficient funds. A defect such as an index computed past the end of a table, or an environmental fault such as the ledger being unreachable, is not something the immediate caller can decide about. Putting it in the same channel forces every function in between to mention it, widens the failure into a bag of unrelated shapes, and buries the three cases that actually matter behind a dozen that do not. It also invites a catch-all branch, which is how a defect becomes a quietly swallowed value instead of a loud stop.
go deeper
Remember the dividing line: failures the operation expects and a caller can act on go in the return type; bugs and environmental faults do not.
Explain the mechanics of sprawl — each shape added deep down appears in every signature and every match on the way up, and pushes callers toward a catch-all branch.
Show the diagnosis: pick a failure shape, ask which caller branches on it, and say what you do with the shapes where the answer is none.
Own the policy. Decide what each boundary's failure vocabulary is, and defend keeping defects loud against a team that wants one uniform result type everywhere.
## Two questions that decide where a failure goes Before a failure is given a shape in the return type, two questions settle whether it belongs there at all. 1. **Is this outcome part of what the operation promises, or a sign that its own assumptions are broken?** "The destination account is not in the directory" is one of the transfer's honest endings. "The fee table was indexed past its end" is not an ending of the transfer; it is evidence that the implementation is wrong. 2. **Does the immediate caller have a decision to make about it?** A caller told the currency is unsupported can offer another currency. A caller told that memory ran out has nothing to choose; the only sensible thing is for the request to stop and for somebody to be told. When both answers point the same way the classification is easy. The interesting cases are the ones where a team answers the first question honestly and the second one not at all, and ends up threading a failure through five layers that each just pass it along. ## What belongs in the channel - **Rejections the domain anticipates**: a non-positive amount, an unsettled currency, an unknown destination, a balance too small. - **Outcomes a caller can route on**: anything where the caller's behaviour genuinely differs between two failure shapes. - **Failures a boundary must render**: the things a response has to explain to whoever made the request. ## What does not - **Broken assumptions inside the implementation.** A violated invariant is not an outcome of the operation; converting it into one launders a bug into ordinary business. - **Faults nobody anticipated.** An unreachable ledger or an exhausted resource is real and important, but the immediate caller has no decision to take on it; it belongs to the boundary that retries, alerts or fails the request. - **Conditions whose only handler is "give up".** If every caller's branch is identical and does nothing, the branch is ceremony and the channel bought nothing. | kind of failure | example in a transfer | where it belongs | what the caller does | |---|---|---|---| | anticipated domain rejection | destination account unknown | the described-failure channel | chooses, reports, or re-routes | | broken internal assumption | fee table indexed past its end | stops the operation loudly | nothing; it is a defect to fix | | environmental fault | ledger unreachable | a boundary that retries or alerts | usually nothing at its own level | ## What sprawl actually costs 1. **Every signature in between.** The failure type appears in each function that passes it, so a shape added deep down edits the vocabulary of layers that never produce it. 2. **Every match.** Callers enumerate shapes they can do nothing with, and the three shapes that matter sit among a dozen that do not. 3. **The catch-all branch.** Faced with shapes it cannot act on, a caller writes one branch for everything else — and that branch now silently absorbs defects too. The loudest signal a defect could give, stopping the operation, has been traded for a value that flows on. 4. **The description dilutes.** A failure type that means both "the user asked for something we do not do" and "we are broken" cannot be rendered, logged or measured as either one. 5. **Review attention.** When every signature mentions failure, the ones where it is interesting stop standing out. ## Keeping the line honest The repair is not to move everything back out again; it is to size the channel per boundary. Each layer declares the small set of failures its callers can decide about, and re-describes anything arriving from below into that vocabulary or lets it stop the operation. A defect kept out of the channel is not ignored — it reaches the place that logs, alerts and fails the request, with its stack and its context intact. That is the value of a failure that is allowed to be loud. A useful test on an existing codebase: take one failure shape and ask which caller branches differently because of it. If the honest answer is "none", it is occupying a channel that costs every layer in between. ## What interviewers listen for - That you can name a failure the immediate caller has **no** decision about, and say what you do with it instead. - That you treat the cost as structural — signatures, matches, the catch-all — rather than aesthetic. - That you do not argue for a codebase with no loud failures at all; a defect that stops the operation is a feature.
- How do you separate an anticipated failure from a defect at design time?Ask whether the operation's contract already accounts for it. A rejected currency is one of the operation's honest endings and has a caller who can choose; reading past the end of a table is a broken assumption inside the implementation and has a caller who can choose nothing.
- Does keeping a defect out of the channel mean ignoring it?No. It means letting it stop the operation and reach the boundary that logs, alerts and fails the request, instead of being converted into an ordinary outcome that flows on. The noise a defect makes is the point of not describing it as a result.
- The failure type has grown a dozen unrelated shapes. What is the repair?Size it per boundary. Each layer publishes the small set its callers can act on and re-describes whatever arrives from below into that vocabulary, so the shapes a caller must match are exactly the ones it can decide about.
saying these in an interview costs you the question
- Argues a codebase with no loud failures at all is automatically better
- Puts a violated internal assumption beside a rejected currency in one channel
- Thinks a wider failure type costs only the function that declares it
- Cannot name a failure the immediate caller has no decision about
- Treats a catch-all failure branch as proof everything is handled
- Keeps a failure shape no caller ever branches differently on