skip to content

The fee calculator throws when a postcode is unserviceable — does throwing break the function's purity?

level: middleimportance: should knowfreq 44%

answer

  1. two properties, not one
  2. determinism versus totality
  3. what decided the throw?
  4. a second exit the signature omits
  5. a timestamp inside the failure value

basics

~20 s

It depends on what decides the throw. A throw fixed by the arguments consults nothing outside and writes nothing outside, so the function stays deterministic — but it stops being total, because some inputs now yield no value at all.

solid answer

~50 s

Separate two properties that the word "pure" usually bundles. **Determinism and observable change**: if the unserviceable postcode is decided entirely from the arguments, the same order throws the same way every time and nothing outside the call is altered, so neither half of purity is violated. **Totality**: the function no longer returns a value for every input in its declared domain — it has a second exit that the return type never advertises, and the caller must handle a path the signature does not mention. That is what a throw really costs here. The picture changes completely if the throw is triggered by something external — a timed-out call to another service, an expiry checked against the clock — because then the *cause* is an undeclared input, and that is the impurity, not the throw itself.

go deeper

for a junior

Know that a function throwing for a bad argument is still returning the same outcome for the same input, and that the surprise for the caller is a path the return type never mentions.

for a middle

Separate determinism from totality out loud, and classify a throw by what decided it: an argument, the clock, or a call to something outside.

for a senior

Show you audit the failure value as well as the condition, and that you account for what runs while the exit propagates — cleanup that writes is in the inventory too.

for a principal

The call worth owning is which definition of purity your codebase writes down, since teams that leave it implicit end up with contracts that say one thing and behave another way at the edges.

## Two properties, only one of which is about effects Candidates answer this question badly because "pure" is doing two jobs in their heads at once. Pull them apart: - **Purity** is the pair of conditions already familiar: the result is fixed by the arguments, and the call changes nothing observable. - **Totality** is a separate property: the function produces a result for *every* input in its declared domain. A function that produces a result for only some inputs is **partial**. A throw decided entirely from the arguments does not touch purity. It reads nothing beyond its parameters and writes nothing that outlives the call. What it does is make the function partial: for that postcode there is no value, only an exit. Interviewers are looking for exactly that separation, because a candidate who says "throwing is a side effect, full stop" cannot then explain why a division that rejects a zero divisor is any different. ## What the throw actually costs The cost is not observable state, it is the **second exit**. The declared result type describes one way out of the function; the throw is another, and nothing in the signature names it. Concretely, for a caller: - the code after the call may never run, and the caller must reason about that at every call site - a value the caller expected to bind is simply absent along that path - the exit propagates outward through frames until something catches it, so the decision about what happens is made somewhere the fee calculator has never heard of None of that is a write to shared state. It is a hole in the function's contract, and it is worth naming as such rather than smuggling it into the word "impure". ## When a throw really is an effect The useful audit question is always: **what decided the throw?** | The throw fires because… | Decided by the arguments? | Verdict | |---|---|---| | the postcode is absent from the rate card | yes | deterministic, but partial | | the order carries no line items | yes | deterministic, but partial | | a call to an outside pricing service timed out | no | impure — the external read is the effect | | the clock says the quoted price has expired | no | impure — an undeclared input decides it | | the failure value it carries holds a timestamp or a fresh identifier | no | impure — two identical calls now differ observably | That last row is the one people miss. If the thrown value is built with the current time, a fresh identifier or a captured snapshot of the machine's state, then two calls with identical arguments produce two observably different outcomes, and the function has an input effect after all — the failure *value* is where it hides. One more nuance to have ready: as the exit propagates, any cleanup attached along the way runs, and if that cleanup writes — closes something, records something, releases something — those are effects. They belong to the cleanup, not to the throw, but they are in the caller's inventory all the same. ## Definitions differ, so say which one you are using There is no single settled answer to "is a partial function pure?" and an interviewer worth the name knows it. Some treatments count any exit that is not a value as an effect, on the grounds that the function's declared type is no longer honest. Others count a deterministic failure as part of what the function *computes*, and reserve "effect" for reads and writes. Both are defensible; what is not defensible is being unaware there is a choice. State the definition you are using, then classify consistently under it. ## Running the audit on a throwing function 1. Identify the condition that fires the throw and check whether every value it reads is a parameter or a local. 2. Inspect how the thrown value is constructed — a timestamp or fresh identifier inside it is an input effect. 3. Ask what runs during unwinding and whether any of it writes. 4. Record the verdict as two lines, not one: pure or impure, and total or partial. The habit that survives the interview is refusing to collapse those two lines into a single word. "Deterministic, effect-free, but partial for postcodes outside the service area" is a complete and checkable description of the fee calculator. "Impure because it throws" is not, and it will lead you to the wrong conclusion the first time you meet a throw whose cause is a network call.

  • How can a throw whose condition is decided purely by the arguments still introduce an input effect?
    Through the value it carries. If the thrown failure is constructed with the current time, a fresh identifier or a snapshot of ambient state, two calls with identical arguments produce observably different outcomes. The condition was deterministic; the payload was not.
  • Why is "it throws, so it is impure" a weak answer even when the verdict happens to be impure?
    Because it names the wrong mechanism. The impurity, when there is one, comes from what decided the throw — an external call, a clock — or from the failure value. Attributing it to the throw itself means you cannot distinguish a rejected argument from a timed-out network read.

saying these in an interview costs you the question

  • Says any function that throws is automatically impure, whatever the cause
  • Claims purity is only about I/O, so a throw is irrelevant to it
  • Uses total and pure as if they were the same property
  • Misses that the failure value itself can carry a timestamp
  • Blames the throw when the real effect is the external call behind it
  • Thinks stack unwinding is itself a write to shared state