skip to content

Effects and Monads

Representing effects and failure as values you can map, combine and sequence, with an explicit channel instead of a thrown exception. Interviewers want the practical payoff, not category theory.

on this pageshow

explore

questions

22

A transfer function returns either a success value or a described failure. What does that return type force on every caller?

level: juniorimportance: must knowfreq 66%

answer

  1. both endings in one value
  2. the signature names the failure
  3. reaching the success means branching
  4. handle, re-describe, or pass onward
  5. a thrown failure obliges no call site

basics

~20 s

Every 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 s

The 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 lines
pseudocode
function 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

for a junior

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.

for a middle

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.

for a senior

Show judgment about depth. Say which failures are worth threading through several layers, and name the boundary where you stop propagating and decide.

for a principal

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
open as a page

A directory lookup returns an empty-or-one-employee context instead of a null reference: what does that force every caller to do?

level: juniorimportance: must knowfreq 78%

basics

~20 s

An empty-or-one-value context puts the missing case in the return type, so a caller cannot reach the employee without opening the container: transform the value inside it, or supply a fallback. The possibility of nothing stops being a convention.

open as a page

An installer builds a value describing fetch, unpack and link instead of performing them — what does that buy the calling code?

level: middleimportance: must knowfreq 58%

basics

~20 s

Building the plan performs no effects, so the code that assembles fetch, unpack and link can be tested, inspected, combined and rewritten as ordinary data. Nothing reaches the network or the disk until a runner is handed the value.

open as a page

A transfer request is validated for amount, currency and destination account — what changes when the response must list every bad field?

level: middleimportance: must knowfreq 58%

basics

~20 s

The three checks stop being a chain and become independent parts combined together. A chain abandons the remaining checks at the first failure; combining runs all three and merges their failures into one description the response can render.

open as a page

When you map a lookup that itself returns a wrapped value over a wrapped value, what comes back, and which operation did you actually need?

level: middleimportance: must knowfreq 62%

basics

~20 s

Mapping a function that itself returns a wrapped value gives you a wrapper nested inside a wrapper. Chaining is the operation you needed: it runs the step and hands back one layer, absorbing the step's own context instead of stacking it.

open as a page

A caller tests whether the manager lookup's result holds a value and then force-opens it: which benefit does that throw away?

level: middleimportance: must knowfreq 66%

basics

~20 s

Testing presence and then force-opening reduces the container to a null check with extra syntax. The branch on emptiness returns to every call site, the duplicated fallback comes back with it, and the unwrap is only correct because a neighbouring statement happens to guard it.

open as a page

A nightly meter import yields one optional reading per row; what does inverting that list of optionals into one optional list give you?

level: middleimportance: must knowfreq 55%

basics

~20 s

Inverting a list of optional readings yields one optional list: present only when every row was present, holding all readings in order. One missing row makes the whole result missing, so the caller unwraps once, not per row.

open as a page

Two configuration settings load independently, neither needing the other's value — which combining operation fits, and what does chaining them instead cost?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Independent loads belong to the combining rung, which takes contexts built side by side and a plain function of their values. Chaining them instead makes the second load a function of the first, so one evaluation order is forced and nothing from the second is available when the first is empty.

open as a page

Your batch import reports only the first bad meter row; what must change for the traversal to report every bad row?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Two things must change: the failure channel needs a way to merge two failures into one, typically a failure holding a list, and the traversal must run every row's step instead of stopping at the first. Collecting needs independent rows.

open as a page

How does handing a runner a value that describes an action differ from simply calling the function later?

level: middleimportance: should knowfreq 48%

basics

~20 s

A postponed call supports exactly one operation — invoking it. A description is data with a fixed set of cases, so several different runners can interpret the same value: one that performs it, one that prints it, one that records or retries it.

open as a page

When a desk-location chain needs a fallback, what changes if you supply it mid-chain rather than at the end?

level: middleimportance: should knowfreq 48%

basics

~20 s

A mid-chain fallback leaves the container early, so every later step runs against a substituted value and can no longer tell a real result from a stand-in. Supplied at the end, the decision about nothing is taken once, by the code that knows what nothing should mean.

open as a page

A nightly import batch arrives with zero rows; what must a traversal over that empty collection return, and why?

level: middleimportance: should knowfreq 36%

basics

~10 s

A traversal over an empty collection returns success holding an empty collection. No step ran, so no step failed; returning the absent context or a failure would report a problem the batch never had.

open as a page

Why can an action value be duplicated, stored and compared freely, while applying that same action twice is not safe?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The description is produced by a pure function, so it can be replaced by its definition anywhere without changing the program's meaning. Applying it is where effects occur, and two applications perform the work twice — the value is a plan, never a cached result.

open as a page

A transfer service routes every failure through its result type, including programming bugs. What does that cost its callers?

level: seniorimportance: should knowfreq 47%

basics

~20 s

Callers 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.

open as a page

A service replaced every null return with an empty-or-one-value context, yet call sites behave identically and the same bugs persist: what actually delivers the benefit?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Changing return types delivers nothing on its own. The benefit comes from changing call sites: chain transformations instead of testing, decide what nothing means once at the edge, never let the container itself be missing, and stop wrapping values that are always present.

open as a page

Parsing every row of an import batch into a fallible result: what does a single traversal do that mapping then inverting does not?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A single traversal applies the step and combines as it goes, building no intermediate collection of wrapped results and calling the step no further than the first failure. Mapping first runs the step on every row.

open as a page

Standardising how teams assemble configuration, you require the weakest of mapping, combining and chaining that expresses each step — what does that buy, and where does the rule cost more than it returns?

level: principalimportance: should knowfreq 28%

basics

~20 s

Requiring the weakest sufficient rung keeps real dependencies visible: a combining step advertises that neither load needs the other, so it may be evaluated in any order and both results stay reachable. It costs review effort, and it stops paying when the work is genuinely sequential.

open as a page

Why model an at-most-one lookup result as a dedicated empty-or-one-value context rather than a list holding zero or one element?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

A zero-or-one collection works mechanically, but it admits a cardinality the domain excludes: two results are representable, so readers and code must rule them out. The dedicated context says at-most-one in the type, and reading it needs no position.

open as a page

Two steps of a transfer chain describe their failures with different types — what must happen before the two compose?

level: seniorimportance: nice to knowfreq 31%

basics

~20 s

A chain yields one value, so its failure side has one shape: each step's failure must first be mapped into a shared description. That is either a wider type with a variant per source, or a re-description in the calling layer's own vocabulary.

open as a page

A teammate rewrites two consecutive mapping stages over a wrapped value as one — which law makes that safe, and when does it break?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

The functor composition law: mapping one function and then another must equal mapping the two as a single function in one pass. The rewrite is safe only while mapping touches nothing but the contained value — a mapping that also counts, logs or re-applies breaks it.

open as a page

Your team proposes that every effectful function in a service return an action value applied only at the entry point — how would you weigh that?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Weigh what a second interpreter is worth against a case set and a runner the team maintains forever. Previews, audits and retries argue for it; opaque stack traces, a sequencing construct for dependent steps, and half-adoption argue against a blanket rule.

open as a page

A nightly meter batch has grown to millions of rows; when should a whole-batch all-or-nothing traversal stop being its shape?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

When the batch stops being the unit the business accepts or rejects. At millions of rows, one bad row discarding every good one, memory held to the end, effects already performed and a late verdict argue for a smaller unit.

open as a page