You already write functions that take several arguments at once: multiply(3, 4). That is fine when you have both numbers in hand. But suppose the 3 is a fixed tax rate you reuse across the whole program, and only the price changes from call to call. You end up repeating multiply(0.07, price) everywhere, and if the rate ever changes you must find every copy.
Currying offers a different shape. Instead of taking all its arguments at once, a curried function takes one argument and returns a new function waiting for the next. Call it once with the rate and you get back a dedicated addTax function; hand that a price and it returns the total. One general function becomes many specialised ones, each pre-loaded with the arguments you already know. The name honours the logician Haskell Curry, though the idea predates him — what matters is the shape, not the history.
flowchart LR A["add(a, b, c)"] --> C["curry"] C --> B["add(a)(b)(c)"] B --> S["one argument per step"] style C fill:#e0900b,color:#fff style B fill:#3776ab,color:#fff
How currying works
A curried function is just a chain of nested closures. The outer function captures its argument in a closure and returns an inner function; the inner captures the next argument and returns another, until the final function has everything it needs and returns the result. Each step along the way is a perfectly good function you can store, pass around, or call later.
The notation gets compact with arrows: (a) => (b) => a + b reads as a function that takes a and returns a function that takes b and returns a + b. Nothing here is exotic — it is the closures from the previous module, used to delay a computation until its last argument arrives.
function multiply(a, b) {
return a * b;
}
// The same logic, curried: one argument per step
function curriedMultiply(a) {
return function (b) {
return a * b;
};
}
console.log(multiply(3, 4)); // 12
const triple = curriedMultiply(3); // fix the first argument
console.log(typeof triple); // function
console.log(triple(4)); // 12
console.log(triple(10)); // 30
Tracing a curried call
Read curriedMultiply(3)(4) right to left as two separate calls. The first, curriedMultiply(3), captures 3 in a closure and hands back a function that is waiting for a second argument. Store it in triple, and triple is now a self-contained function — it carries the 3 with it wherever it goes. The second call, (4), feeds 4 to that stored function, which multiplies its captured 3 by 4 and returns 12.
Currying versus partial application
The two terms are related but not identical. Currying transforms a function so it accepts arguments one at a time, fully. Partial application is the broader idea: fix some arguments now, get back a function for the rest, however many remain. Every curried call is a partial application, but you can partially apply a normal function without currying it at all.
JavaScript gives you a built-in tool for this: fn.bind(null, fixedArg) returns a copy of fn with the first arguments already supplied. You can also do it by hand with a closure. Either way, you are pre-filling the arguments you already know and leaving the rest open. Think of currying as a full conversion of a function's shape, and partial application as a convenient fix-up you can apply to any function.
flowchart LR F["add(a, b)"] --> P["fix a = 10"] P --> G["add10(b) = 10 + b"] G --> R["add10(5) = 15"] style P fill:#b45309,color:#fff style G fill:#1e7f3d,color:#fff
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
// By hand: pre-fill `greeting` with a closure
function sayHi(name) {
return greet("Hi", name);
}
// With bind: fix the first argument in one call
const sayYo = greet.bind(null, "Yo");
console.log(sayHi("Ada")); // Hi, Ada!
console.log(sayYo("Linus")); // Yo, Linus!
When it helps (and when it does not)
Currying earns its keep when one argument is stable and another varies — a logger with a fixed level, a fetcher with a fixed base URL, a formatter with a fixed locale. Pre-fill the stable piece and pass the specialised function around instead of repeating the constant. It also glues neatly into map and filter, which expect single-argument callbacks; mapping triple over an array reads as nums.map(triple) — no anonymous callback, no repeated 3, just the specialised function doing its job.
The trap is overuse. Do not curry a function you only ever call with every argument at once — the extra calls add ceremony and confusion for no benefit. Reach for currying when it removes repetition; leave it alone when a plain add(a, b) already says everything.
flowchart LR X["10"] --> A["add10"] A --> Y["20"] Y --> B["double"] B --> Z["40"] style A fill:#3776ab,color:#fff style B fill:#e0900b,color:#fff
const multiply = (a) => (b) => a * b; const double = multiply(2); const triple = multiply(3); const nums = [1, 2, 3]; console.log(nums.map(double)); // [2,4,6] console.log(nums.map(triple)); // [3,6,9] console.log(nums.map((n) => double(triple(n)))); // [6,12,18]
Write a curried function multiply(a) that returns a new function. That returned function takes b and returns a * b. So multiply(3)(4) is 12.
function multiply(a) {
// return a function of b that returns a * b
}
The body should
return function (b) { ... }so the first call gives back a function.That inner function closes over a, so it remembers it until the second call.
function multiply(a) {
return function (b) {
return a * b;
};
}
What does this print? Each call to add takes one argument and returns a function until it has all three.
function add(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
console.log(typeof add(1));
console.log(typeof add(1)(2));
console.log(add(1)(2)(3));
add(1) returns a function, so typeof is the string "function".
Only once all three arguments are supplied does the innermost body run and return a number.
Write curry2(fn) that turns any two-argument function into a curried one. So if add = (a, b) => a + b, then curry2(add)(2)(3) is 5, and curry2(add)(10) is a function still waiting for its second argument.
function curry2(fn) {
// return a => b => fn(a, b)
}
Return a function of a, which returns a function of b, which calls fn(a, b).
curry2(add)(10) stops at the first function — calling it again with 5 finishes the job.
function curry2(fn) {
return function (a) {
return function (b) {
return fn(a, b);
};
};
}
This greet is meant to be curried: greet("Hi")("Ada") should return "Hi, Ada!". But it returns the string too early — it is missing a level of functions. Fix it so the first call returns a function that is still waiting for the name.
function greet(greeting) {
return `${greeting}, friend!`; // returns too early — not curried
}
The body must return a function of name, not a string.
That inner function closes over greeting and builds the final string.
function greet(greeting) {
return function (name) {
return `${greeting}, ${name}!`;
};
}
Given const f = (a) => (b) => a + b, which call correctly produces the number 7?
f is curried: it takes one argument and returns a function waiting for the second. f(3)(4) supplies 3, then 4, and the inner function returns 7. f(7) returns a function, not 7, and f ignores any second argument handed to its first call.
Recap
- Currying turns a multi-argument function into a chain that takes one argument at a time:
f(a)(b)(c)instead off(a, b, c). - It is built from nested closures — each step captures one argument and returns a function for the rest.
- Partial application is the broader idea: fix some arguments now, leave the rest open.
fn.bind(null, fixed)does this in one call. - Currying pays off when one argument is stable and another varies; it is noise when you always pass every argument anyway.
Next you'll learn memoization, which uses the same closure trick to remember a function's answers and skip repeat work.