skip to content

A permission check branches on allow, deny and escalate with a catch-all default — what does that default hide later?

level: middleimportance: must knowfreq 64%

answer

  1. cover the set, or cover the gap
  2. a closed set can be enumerated
  3. the default absorbs what you forgot
  4. silence at the point of change
  5. fail loudly instead of returning plausibly

basics

~20 s

A catch-all default makes any outcome added later fall silently into an existing arm, so nothing reports the missing case. Listing every outcome explicitly, with no default, turns that later addition into a loud failure at the point of change instead.

solid answer

~50 s

Multi-way selection over a **closed set** of outcomes can be covered in two ways, and they fail very differently. A catch-all default arm covers the set by construction: every present and future outcome matches something, so the selection is total and stays total no matter what is added to the set. That is exactly the problem — when a fourth outcome such as *quarantine* is introduced, the permission check keeps compiling, keeps running, and quietly treats it as whatever the default did, probably deny. Enumerating one arm per outcome with no default gives up that guarantee on purpose: where exhaustiveness is checked, the new outcome becomes an error at the point of the change, and the person adding it is told every place that must decide what to do about it. A default is right when the set is genuinely open; it is wrong when the set is closed and the default is really "I have not thought about the rest".

code

pseudocode · 10 lines
pseudocode
# outcome is drawn from a closed set the program itself defines
function act(outcome)
    select outcome
        case ALLOW    -> return grant()
        case DENY     -> return refuse()
        case ESCALATE -> return sendToReviewer()
        default       -> return refuse()

# add QUARANTINE to the set later: this function still builds,
# still runs, and refuses every quarantined request without a word

go deeper

for a junior

Know what the default arm is for: it runs when no earlier arm matched, and it makes the selection cover every possible input.

for a middle

Explain the trade — permanent totality against the loss of a missing-case signal — and say how a closed set differs from an open one.

for a senior

Describe finding this in a live system: a new outcome silently denied for weeks, and the change that turns the last arm from a plausible answer into a loud failure.

for a principal

Own the convention across teams: where closed sets may be branched on directly, how many selections may share one, and what the review asks when a default appears.

## Two ways to cover a closed set A **closed set** of outcomes is one the program itself defines and can enumerate: allow, deny, escalate. Multi-way selection over it must be **total** — every input takes some arm — and there are two ways to achieve that. 1. **Cover by catch-all.** List the arms you thought of and finish with a default that absorbs everything else. Totality is guaranteed structurally, forever. 2. **Cover by enumeration.** List exactly one arm per member of the set and no default. Totality now depends on the arms matching the set, which is a property that can be **checked** — and that can break when the set grows. The second looks more fragile and is the safer one, because the fragility is the alarm. | | Catch-all default | One arm per outcome, no default | |---|---|---| | A fourth outcome is added | absorbed silently by the default | reported as a missing case where exhaustiveness is checked | | Where you find out | in production, as wrong behaviour | at the moment of the change | | Who is told | whoever debugs the incident | whoever adds the outcome | | Cost | a permanently total selection | every addition touches every selection | ## What the default arm actually does The default arm answers a question nobody asked out loud: *what should happen to an outcome I have not considered?* In a permission check the honest answer is usually "I do not know", but a default has to pick something, and picking `deny` feels safe. It is not safe in the way it looks: - **Deny by default hides an escalation.** The quarantine outcome added last quarter may need a human reviewer; denying it is a silent policy decision made by code written before the outcome existed. - **Allow by default hides a breach.** The same structure with the opposite arm turns an unknown state into access. - **Either one hides the change itself.** The reviewer of the commit that adds the fourth outcome sees no diff in this file, because there is nothing to change. The cost is not that the default is wrong on the three known outcomes — it never runs for them. The cost is that it converts *"a case is unhandled"*, which is a checkable property, into *"a case is handled by whatever I wrote once"*, which is not. ## Exhaustiveness as a checked obligation Where a language checks exhaustiveness, an enumerated selection with no default is a **contract with the checker**: you promise to have an arm per outcome, and the checker holds you to it every time the set changes. Languages differ in how far this goes — some check exhaustiveness of multi-way selection over a closed set and refuse to build otherwise, some warn, and some offer no such check at all — but the discipline is available even without a checker: - Keep a **"must not happen" arm** that fails loudly (raises, aborts, alerts) rather than returning a plausible value. It is still a catch-all, but it is a catch-all that reports. - Add a **test that asserts one case per outcome**, iterating the set, so a new member fails a test rather than passing silently. - Keep the set and its selections close together, so the addition and the arms are in the same review. The loud arm is the important half. A default that denies looks identical to a default that alerts, and only one of them tells you the world changed. ## When a default is the right call A default is not a smell by itself. Reach for it when the set is genuinely **open** — input parsed from outside, a value received from another system, a numeric range, anything whose members are not fixed by your program. There, "everything else" is a real category and the default arm is the correct handling of an input you cannot enumerate. The test to apply is simple: *can I list every member of this set today, and does the list live in my code?* If yes, the set is closed and the default is borrowing against the future. If no, the default is doing its job. ## The argument to make in an interview State the trade cleanly: a default buys permanent totality and pays for it by deleting the signal that a new case exists; an enumerated selection gives up permanent totality to keep that signal. Then say which one the situation deserves, and name the loud-failure arm as the fallback when the language will not check for you.

  • The language will not check exhaustiveness for you. What do you put in the last arm?
    An arm that fails loudly — raise, abort, or emit a high-severity alert naming the unhandled value — rather than one that returns a plausible decision. It is still structurally total, so nothing breaks at build time, but the first request carrying a new outcome reports itself instead of being quietly refused.
  • Does adding an arm per outcome scale when twenty selections branch on the same set?
    It scales badly on purpose: every addition touches all twenty, and that is the signal. Where most of them share a decision, reduce the count first — push the branching into one place that maps the outcome to a decision, and let the other nineteen consume that. Then exhaustiveness is checked once.
  • When is a catch-all default clearly the right choice?
    When the set is open — a value parsed from outside, received from another system, or drawn from an unbounded range. You cannot enumerate its members, so "everything else" is a genuine category and handling it explicitly is correct rather than lazy.

A mailroom with one "unsorted" bin never reports a problem: mail for a department created last month lands in the bin and looks handled. Remove the bin and somebody is forced, on the first delivery, to say where the new department's mail goes.

saying these in an interview costs you the question

  • Says a default arm always makes a selection safer
  • Treats deny-by-default as a decision nobody has to review
  • Believes an unhandled case will show up as an error anyway
  • Cannot distinguish a closed set of outcomes from an open one
  • Thinks exhaustiveness is about arm order rather than coverage
  • Uses a catch-all that returns a plausible value instead of failing