InterviewsVector
intermediateVery common5 min read · Updated Jul 18, 2026

Implement curry(): The Classic Closure Exercise

Implement a curry function in JavaScript with arity collection, the fn.length gotchas with defaults and rest params, and the infinite-sum variant interviewers add.


The problem

Implement curry(fn) so that a 3-argument function can be called as f(1)(2)(3), f(1, 2)(3), f(1)(2, 3), or f(1, 2, 3) — all returning the same result.

Currying is the canonical closure exercise: the state being closed over is "the arguments collected so far," and every partial call creates a new closure holding a longer prefix. If you can narrate where those arguments live and why sibling partials don't interfere, the implementation writes itself.

Implementation

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);      // enough arguments — actually run
    }
    return function (...nextArgs) {     // not yet — return a collector
      return curried.apply(this, [...args, ...nextArgs]);
    };
  };
}

The whole mechanism is one comparison: args.length >= fn.length — "do I have as many arguments as the function declares?" Each partial call recurses into curried with the accumulated prefix, so every stage gets the same decision logic.

Dry run

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
 
curriedAdd(1)(2)(3);
// call 1: args=[1]     1 < 3 → return collector closing over [1]
// call 2: args=[1,2]   2 < 3 → return collector closing over [1,2]
// call 3: args=[1,2,3] 3 >= 3 → add(1,2,3) → 6
 
const add1 = curriedAdd(1);
add1(2)(10); // 13 — and add1 is REUSABLE:
add1(5)(10); // 16 — each call from add1 builds its own fresh chain

That reusability is the point worth saying out loud: partials are values. curriedAdd(1) isn't a step in a protocol — it's a new function you can store, pass, and reuse, and none of its descendants share mutable state.

The fn.length gotchas (where interviewers poke)

The elegance rests on fn.length — and fn.length is more fragile than people think:

((a, b) => {}).length;        // 2
((a, b = 1) => {}).length;    // 1 — stops counting at the first default!
((a, ...rest) => {}).length;  // 1 — rest params don't count
((a, b) => {}).bind(null, 1).length; // 1 — bind subtracts bound args

So curry((a, b = 2) => a + b) fires after one argument, and a rest-param function can never collect more than its declared count. The honest fix is an explicit arity: curry(fn, arity = fn.length). Volunteering this limitation — and the parameter that patches it — is the difference between having memorized curry and understanding it.

Curry vs partial application

Constantly conflated, and a guaranteed follow-up:

CurryingPartial application
Transformsone n-ary fn → chain of calls collecting argsone n-ary fn → one fn with some args pre-filled
Calls to get a resultas many as it takes to reach arityexactly one more
f(1) returnsanother collector (until arity)— you chose the split up front: partial(f, 1)

One-liner: currying is automatic, repeatable partial application. In production JavaScript you'll use ad-hoc partials (arrow wrappers, bind) far more often than true currying — configuration-then-data pipelines (const format = formatWith(locale); items.map(format)) are the realistic use, and saying so beats overselling FP purity.

The variant to expect: infinite currying

Make sum(1)(2)(3)...() work — terminate on the empty call.

No arity to compare against, so termination needs an explicit signal:

function sum(...args) {
  const total = args.reduce((a, b) => a + b, 0);
  const next = (...more) =>
    more.length === 0 ? total : sum(total, ...more);
  return next;
}
 
sum(1)(2, 3)(4)(); // 10

The interviewer's real question is "how do you end an unbounded chain?" — empty call, .value() method, or coercion via Symbol.toPrimitive/valueOf (so sum(1)(2) + 0 works). Name at least two; implement one.

Edge cases interviewers probe

  • Extra argumentscurriedAdd(1, 2, 3, 4): pass through (>=, not ===) and let fn ignore extras, matching plain JS call semantics; a strict-arity variant is a stated policy, not a fix.
  • this forwarding — the apply(this, ...) chain matters if anyone curries a method; also why the collectors are functions, not arrows.
  • Zero-arg functionscurry(() => 42)() must work: 0 >= 0 on the first call. Falls out naturally; know that it does.
  • Each partial is independent — two branches from one partial never share or overwrite arguments (fresh array per call: [...args, ...nextArgs], never push).

Common mistakes

  • Mutating the collected array (args.push(...)) — sibling partials now corrupt each other; the spread-copy is load-bearing.
  • Not knowing the fn.length quirks and being unable to explain why curry broke on a defaulted parameter.
  • Explaining currying as "performance" — it's an ergonomics/composition tool; the performance-adjacent cousin is memoization.
  • Arrow functions for the collectors when this forwarding is in scope.
  • Conflating curry with partial in the verbal explanation (the table above is the fix).

Follow-up questions

  • "Add placeholder support so arguments can arrive out of order." — the real escalation, with its own trap: currying with placeholders.
  • "Curry a variadic function." — impossible via fn.length; explicit arity parameter — same discussion as the gotchas above.
  • "TypeScript types for curry?" — honest answer: fully general curry types need recursive conditional types (Parameters, tuple slicing) and get hairy fast; typing fixed arities (curry2, curry3) is the pragmatic move. Knowing where the type system strains is itself signal.
  • "Where does curry show up in real libraries?" — Ramda (all functions curried), Redux's connect(mapState)(Component), middleware signatures like store => next => action — reading those is reading curry.
  • "Compose curried functions?" — currying is what makes point-free pipe pipelines practical: unary stages compose; curry manufactures unary stages.

  • pipe()

    intermediate

    Compose functions left-to-right into a pipeline — the one-liner with a lot of interview depth.

  • _.partial()

    intermediate

    Pre-fill leading arguments of a function — partial application, and how it differs from currying.

  • _.once()

    beginner

    Guarantee a function runs exactly once and returns its cached result forever after.