skip to content

A caller of a curried permission check omits one argument - why does that bug surface far from the call site?

level: seniorimportance: nice to knowfreq 30%

answer

  1. under-application is legal, not an error
  2. a function arrives where a decision was expected
  3. the value travels before anything notices
  4. failure surfaces at the consumer
  5. a missed check can fail open

basics

~20 s

Because an under-applied curried call is well formed: it returns the next rung instead of failing. That function value then travels - stored, returned, passed on - and the trouble appears wherever something expects a decision and meets a function.

solid answer

~50 s

A curried chain has no notion of "too few arguments". Every rung is a one-argument function, so supplying two of three is a legitimate call that yields the third rung rather than an error. What you now hold looks like any other value: it can be put in a handler table, returned from a factory, logged, or tested in a condition. The failure appears at the point of *use*, which may be layers away and long after - and its worst form is failing open, where a setting that treats any non-empty value as true reads that function as an allow and lets the request through unchecked. A plain three-parameter check would have rejected the short call immediately, at the site of the mistake. The cure is to keep chains shallow, name every retained rung for what it already carries, and force the value back to a decision at a defined boundary.

code

pseudocode · 7 lines
pseudocode
// curried check expects role, then resource, then action
handler = curried(requestRole)(documentResource)
// two rungs supplied out of three: handler is still a function

if (handler)
    allow()      // condition passes: any non-empty value counts as true
// the action was never examined - the request is allowed unchecked

go deeper

for a junior

Remember that a curried call with missing arguments returns a function instead of failing, so the mistake is not reported where it is made.

for a middle

Explain the mechanism: every rung is a complete one-argument call, so there is no arity error to raise and the leftover function is an ordinary value that gets passed on.

for a senior

Show how you would contain it in a real service - shallow chains, named rungs, one boundary that refuses anything still callable, and tests that assert denials rather than only allows.

for a principal

Judge where the style is worth its diagnostic cost. On a security decision the silent fail-open direction argues for an explicit, fully applied check at the boundary.

This is the operational cost of currying, and it is the reason experienced teams keep chains short even where the mechanism is idiomatic. ## Under-application is not an error A permission check over `role`, `resource` and `action`, curried, is not a three-parameter function. It is a one-parameter function that returns a one-parameter function that returns a one-parameter function. So a caller who supplies the role and the resource and stops has made **three well-formed calls short of nothing**: it made two, and each was complete. There is no arity mismatch to report, because no rung was called with the wrong number of arguments. Contrast a plain three-parameter check. Omitting an argument there is a malformed call, caught at the call itself - by the compiler where the language checks it, or by the call machinery where it does not. The diagnosis points at the line that made the mistake. | | Plain three-parameter check | Curried chain | |---|---|---| | Short call is | malformed | well formed | | Detected | at the call | when the result is used, if at all | | Report names | the offending call site | the consumer that got the wrong thing | | Worst outcome | an immediate failure | a request allowed without a check | ## How the wrong value travels The function value produced by the short call is first-class, which is exactly what makes it dangerous here. It can be: - stored in a map of handlers alongside genuine decisions, - returned through several layers of factory or wiring code, - passed to a function whose parameter is only loosely constrained, - written to a log, where it prints as something unhelpful rather than as a verdict, - tested in a condition, which is the failure mode that matters. That last one is the fail-open case. Where a runtime treats any non-empty value as true in a condition, `if (handler)` succeeds for a function value exactly as it would for an allow, and the request proceeds although the action was never examined. Nothing crashed, nothing was logged, and the security control was silently skipped. ## Why the report points at the wrong place Where the mismatch *is* caught - a setting that checks what a value may be used as, or a run-time failure when something tries to treat the function as a verdict - the place it is caught is the **consumer**. The consumer is correct; it asked for a decision. The mistake was made by whoever built the value, possibly in another module and possibly at start-up rather than during the request. Stack information gathered at the point of failure describes the innocent party. ## Containing it 1. **Keep the chain shallow.** Two or three rungs are comprehensible; five are a puzzle, and the deeper the chain the more prefixes exist that look like finished values. 2. **Name every rung you retain.** A value called `checkerForDocuments` announces what is already fixed and what is missing; an anonymous one announces nothing. 3. **Collapse at a boundary.** Decide the one place where a chain must become a decision, and make that boundary assert it - anything still callable there is a defect, not a value to forward. 4. **Prefer one-step specialisation where the shape is fixed.** Fixing two arguments in one named wrapper produces a function whose remaining parameter list is obvious, rather than a rung you have to count back to interpret. 5. **Test the negative path.** A test that asserts a denied action is actually denied catches a fail-open check; a test that only asserts allows will pass happily against a function value. ## The general lesson Currying trades an immediate, local error for a deferred, distant one. That trade is usually worth making where the values are ordinary data and a wrong shape is caught quickly. It is worth much more caution where the value being produced is a **security decision**, because the deferred failure can be silent and the safe direction is not the default one.

  • What keeps this from happening in a codebase that leans on currying?
    Shallow chains, a name on every retained rung that says what it already carries, and one boundary where a chain must become a decision - with that boundary rejecting anything still callable. Tests that assert denials, not only allows, catch the fail-open case that review misses.
  • How does a statically checked setting change the picture?
    It usually catches the mismatch, but at the place the value is used rather than where it was built, because the short call was itself well formed. The diagnosis names the innocent consumer, so the investigation still has to walk back to the producer.

saying these in an interview costs you the question

  • Says a curried call with too few arguments always raises immediately.
  • Thinks the missing argument falls back to a neutral default.
  • Assumes the failure report names the call that omitted the argument.
  • Treats a leftover rung as automatically equivalent to a denial.
  • Claims deep chains cost nothing in readability or diagnosis.