A transfer function returns either a success value or a described failure. What does that return type force on every caller?
answer
- both endings in one value
- the signature names the failure
- reaching the success means branching
- handle, re-describe, or pass onward
- a thrown failure obliges no call site
basics
~20 sEvery caller must handle the failure case to reach the success value, because the two outcomes are alternative shapes of one returned value. A failure that is thrown instead puts no such obligation at the call site.
solid answer
~40 sThe return type is a single value that is *either* a success carrying the confirmation *or* a failure carrying a description of what was wrong. The confirmation only exists inside the success shape, so a caller that wants it has to ask which shape it received, and answering that means writing something for the failure shape too. The honest options are to handle it, to re-describe it, or to pass it onward — and passing it onward shows up in the caller's own return type. A thrown failure appears nowhere in the signature, so a silent caller still compiles. The guarantee is narrower than people claim: the success value is unreachable without a branch, but the whole returned value can still be discarded unless the language complains.
code
pseudocode · 12 linesfunction transfer(request)
if request.amount <= 0
return Failure("amount must be positive")
if not settles(request.currency)
return Failure("currency not supported")
return Success(confirm(request))
# the caller cannot reach the confirmation without naming both shapes
result = transfer(request)
match result
Success(confirmation) -> receipt(confirmation)
Failure(reason) -> report(reason)go deeper
Recall that the return type has two alternative shapes and the success value lives inside only one of them. Be ready to say, in words, what the calling code has to write to get at it.
Explain the mechanics: the value is one shape at a time, opening it must name both, and choosing to pass the failure onward changes the caller's own return type.
Show judgment about depth. Say which failures are worth threading through several layers, and name the boundary where you stop propagating and decide.
Frame it as a team contract. Weigh the reviewability a visible failure buys against the signature churn it imposes on every layer in between, and say when you would mandate it.
## Two endings in one returned value A money transfer has more than one honest ending. It can succeed and produce a confirmation, and it can fail in ways the business already anticipates: the amount is not positive, the currency is not one the service settles in, the destination account is not in the directory. A **typed error channel** puts both kinds of ending into the function's declared return type — one value that is *either* a success carrying the confirmation *or* a failure carrying a description of what went wrong. The load-bearing word is *either*. The two shapes are alternatives, not a pair: at run time the returned value is one of them and never both, and the confirmation exists only inside the success shape. That is what turns "this can fail" from a sentence in the documentation into a fact stated by the signature, checked by whatever checks types. ``` function transfer(request) # gives back Success(confirmation) or Failure(reason) if request.amount <= 0 return Failure("amount must be positive") if not settles(request.currency) return Failure("currency not supported") return Success(confirm(request)) ``` ## What it forces on the caller 1. **Open the value by shape.** The caller cannot read the confirmation directly. It has to ask which shape came back, and answering that question means writing something for the failure shape as well as the success shape. 2. **Decide here and now.** The decision has three honest forms: handle the failure (show it, pick a default, take another route), convert it into the caller's own vocabulary, or hand it onward unchanged. 3. **Record the decision in its own type.** Handing it onward is not free. The caller's own return type now has to carry a failure too, which is exactly why the channel stays visible all the way up to the layer that finally acts. None of this is a run-time mechanism. Nothing unwinds, no stack is walked, no handler is searched for. A returned failure is an ordinary value travelling the ordinary return path, and the code that inspects it is an ordinary branch. ## Compared with a failure that is thrown | | failure described in the return type | failure that is thrown | |---|---|---| | visible in the signature | yes, as one of the two shapes | usually not, or only partly | | a silent caller | cannot reach the success value | compiles, and the failure travels past it | | where it surfaces | at the call site | wherever the nearest handler happens to be | | control flow | ordinary values and ordinary branches | a separate path that skips the code between | | functions in between | mention the failure in their types | say nothing about it | | a newly added failure shape | callers that enumerate the shapes must be revisited | no signal at all | Read that table in both directions. What the left column buys in visibility it pays for in plumbing: every function between the failure and the layer that can act on it mentions the failure. What the right column buys in quiet it pays for at the call site, where nothing warns a reader that this call can end badly. ## What the channel does not buy you - **It does not make the failure impossible to drop.** A caller can discard the whole returned value wherever a language permits an unused return value. The real guarantee is narrower and still worth having: the success payload cannot be read without a branch. Warnings on unused results and ordinary review close the rest of the gap. - **It does not describe the failure well by itself.** A failure shape carrying one message string is barely better than a flag. A caller that must react differently to an unknown account than to an unsupported currency needs failure shapes it can tell apart. - **It does not remove the boundary handler.** The channel carries the failure to somewhere a decision is possible; something at that boundary still turns it into a response, a retry or a log line. - **It is not free at depth.** Threading a failure through five layers that can do nothing with it is the cost people genuinely complain about, and dismissing that cost is a weaker answer than acknowledging it. - **It says nothing about faults nobody anticipated.** The channel states the outcomes this operation has. An assumption broken inside the implementation is not one of its outcomes. ## Why an integer status code is not the same thing Returning a number beside the value, with zero meaning success, looks similar and behaves differently. The value and the code are both present at once, so nothing prevents a caller from reading the value and never looking at the code — the compiler sees two perfectly usable results. With two exclusive shapes there is no value to read until the shape is known. That difference, not the richness of the description, is what makes the channel hard to bypass. ## What interviewers listen for - That you can say *what the caller actually writes*, rather than asserting the call is "type-safe". - That you separate the real guarantee (the success value sits behind a branch) from the overstatement ("it can never be ignored"). - That you can name the layer where you would stop threading the failure and make a decision.
- What stops a caller from discarding the returned value entirely?Nothing in the shapes themselves. Some languages warn or refuse when a returned value goes unused, and that is what closes the gap; without it, the guarantee is only that the success payload cannot be read without a branch. Saying this out loud is stronger than claiming the failure is unignorable.
- If the caller cannot act on the failure, what should it do with it?Pass it onward, re-described in its own vocabulary if the layer above should not see the lower layer's terms. That is a decision, not an evasion: it moves the failure to the layer that can choose, and it is recorded in the caller's own return type rather than hidden.
- Does returning a failure abandon the rest of the function?Only because the `return` does. Producing a failure is building a value; nothing unwinds and nothing is skipped implicitly. Code after the branch that produced it runs exactly as ordinary control flow says it does, which is the difference from a thrown failure.
A sealed envelope marked either receipt or rejection notice: you cannot read the receipt without opening it, and opening it means finding out which one you were sent.
saying these in an interview costs you the question
- Thinks returning a failure unwinds the stack the way a throw does
- Claims a returned failure can never be ignored, whatever the caller writes
- Describes it as a status flag sitting beside the value
- Cannot say what the caller must write to reach the success value
- Argues every failure in the system belongs in the return type
- Treats a single message string as a well described failure