Learn JS Series (#26) - Currying and Partial Application
What will I learn
- You will learn what currying is: turning a multi-argument function into a chain of single-argument functions;
- the difference between currying and partial application, which are related but not the same;
- how closures make both possible, and how to write them by hand;
- how to build a generic
curryhelper that works on any function; - why these techniques enable clean, reusable, configurable functions.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-25 read, especially closures, higher-order functions, and bind.
Difficulty
- Intermediate
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
- Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
- Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
- Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)
- Learn JS Series (#26) - Currying and Partial Application (this post)
Learn JS Series (#26) - Currying and Partial Application
Solutions to Episode 25 Exercises
Exercise 1 - a private-sum IIFE:
const shown = (function () {
let total = 0;
for (let i = 1; i <= 10; i++) total += i;
return total;
})();
console.log(shown); // 55
// i and total do not exist out here
The insight: the IIFE runs immediately and its locals vanish, so only the returned value escapes into shown.
Exercise 2 - a counter module:
const counter = (function () {
let count = 0;
return {
increment() { return ++count; },
decrement() { return --count; },
value() { return count; },
};
})();
console.log(counter.increment(), counter.increment(), counter.decrement()); // 1 2 1
The insight: count is private to the IIFE; the returned methods are the only way to touch it.
Exercise 3 - the revealing variant:
const counter2 = (function () {
let count = 0;
function increment() { return ++count; }
function decrement() { return --count; }
function value() { return count; }
return { increment, decrement, value }; // public API in one place
})();
console.log(counter2.increment()); // 1
The insight: listing the public names in one return makes the API obvious at a glance; a real ES module would give this privacy without the wrapper.
Last episode we saw how closures let an IIFE hide state and hand back a controlled public surface. Today we lean on that exact same closure machinery again, but point it at a different problem: how to shape a function's arguments. What we cover now, currying and partial application, are two closely related functional techniques that build directly on the bind partial application you already met in episode 23. Having said that, they are two of those ideas that sound academic until the moment they click, and then you start spotting places for them everywhere. Let's build them from the ground up.
Partial application: fixing some arguments now
Partial application means taking a function with several arguments and fixing some of them ahead of time, producing a new function that only needs the rest. You saw this with bind in episode 23; here we do it explicitly with a closure, which shows what is really happening under the hood:
function multiply(a, b) {
return a * b;
}
function partial(fn, fixedArg) {
return function (remaining) {
return fn(fixedArg, remaining); // 'fixedArg' is remembered via closure
};
}
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
partial(multiply, 2) returns a function that has already locked in 2 as the first argument; you only supply the second. The fixed argument survives in the closure (episode 17), waiting patiently for the rest to arrive. This is exactly what multiply.bind(null, 2) does - only now you can see the closure machinery underneath instead of trusting bind to do it invisibly. Partial application is about pre-filling some arguments to specialize a general function.
Let me make the connection to bind concrete, because it is worth seeing the two side by side. These two lines produce functionally identical double helpers:
function multiply(a, b) { return a * b; }
const doubleViaBind = multiply.bind(null, 2); // built-in partial application
const doubleViaClosure = (b) => multiply(2, b); // hand-rolled, same effect
console.log(doubleViaBind(9), doubleViaClosure(9)); // 18 18
The null in bind is the this value (episode 23), which a plain function like multiply does not care about, so we pass null. Everything after that first argument is a fixed argument. So bind was doing partial application all along - we just did not have the vocabulary for it yet. Now we do.
Currying: one argument at a time
Currying is a stricter, more systematic transformation (named after the logician Haskell Curry, which is also where the Haskell language got its name). To curry a function is to turn a function that takes N arguments all at once into a chain of N functions that each take exactly one argument, returning the next function until all are collected, then finally computing the result:
// a normal three-argument function:
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
// the curried version: one argument per call
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c; // all three captured via nested closures
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
Read curriedAdd(1)(2)(3) left to right and it is not mysterious at all: curriedAdd(1) returns a function waiting for b; calling that with 2 returns a function waiting for c; calling that with 3 finally has everything it needs and computes. Each nested function closes over the arguments gathered so far, which is why a is still visible three levels deep. Currying is about taking arguments one at a time through a chain of functions.
The difference from partial application is subtle but real. Partial application fixes some arguments and leaves a function that takes the rest all at once. Currying insists on exactly one argument per call, all the way down the chain. Put another way: currying is the disciplined, uniform version (always one arg, always a function back until the end), while partial application is the pragmatic "fix whatever you like, take the remainder together" version. They are cousins, not twins.
Why one argument at a time is useful
Currying looks like extra ceremony until you notice what it quietly hands you: every partially-applied stage is a reusable, specialized function. Because a curried function returns a new function after each argument, you can stop partway and reuse that intermediate function as many times as you like:
function curriedGreet(greeting) {
return function (name) {
return `${greeting}, ${name}!`;
};
}
const sayHi = curriedGreet("Hi"); // stop after the first argument
const sayBye = curriedGreet("Bye");
console.log(sayHi("scipio")); // "Hi, scipio!"
console.log(sayHi("alice")); // "Hi, alice!"
console.log(sayBye("scipio")); // "Bye, scipio!"
curriedGreet("Hi") gives you a reusable sayHi you can apply to a hundred different names without ever repeating the greeting. This is the everyday value of currying: it turns configuration into specialized functions naturally. You configure once (the greeting) and reuse forever (the name). It also composes beautifully with the pipelines we build next episode, because curried functions plug into one another cleanly - a function that always takes one argument and returns one value is the ideal Lego brick.
Here is a slightly more realistic taste of that specialization, one you will genuinely reach for. Imagine a logger that needs a level and a message. Curry it, and each log level becomes its own tidy function:
const log = (level) => (message) => `[${level.toUpperCase()}] ${message}`;
const info = log("info"); // pre-configure the level once
const warn = log("warn");
const error = log("error");
console.log(info("server started")); // "[INFO] server started"
console.log(warn("disk almost full")); // "[WARN] disk almost full"
console.log(error("connection lost")); // "[ERROR] connection lost"
Notice how info, warn, and error read like purpose-built helpers, yet there is only ONE tiny log function doing the real work. That is currying earning its keep: less repetition, clearer intent.
A generic curry helper
Writing nested functions by hand for every case is tedious, and nobody wants to hand-roll a three-deep closure every time. So we write a single curry helper that turns any function into a curried one automatically. The trick is delightfully small: keep collecting arguments until we have as many as the original function expects (its .length property, which is the number of declared parameters), then call it for real:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args); // enough args -> call the real function
}
return (...more) => curried(...args, ...more); // otherwise gather more
};
}
function add3(a, b, c) {
return a + b + c;
}
const curriedAdd3 = curry(add3);
console.log(curriedAdd3(1)(2)(3)); // 6
console.log(curriedAdd3(1, 2)(3)); // 6 - flexible: any grouping works
console.log(curriedAdd3(1)(2, 3)); // 6
console.log(curriedAdd3(1, 2, 3)); // 6 - even all at once
This curry is wonderfully flexible: it accepts arguments in any grouping, gathering them across calls until it finally has enough. fn.length gives the expected count (for add3 that is 3), and the rest/spread operators ...args and ...more (from episode 20) do all the collecting. The named function expression curried lets the inner arrow call back into itself recursively (episode 24) - so this one little helper is closures, plus recursion, plus rest/spread, all working together. It is a genuine showcase of everything we have learned this phase.
Let me trace one call slowly so the recursion is not a black box. Take curriedAdd3(1)(2)(3):
// step 1: curriedAdd3(1)
// args = [1], fn.length = 3, 1 >= 3 is false
// -> returns (...more) => curried(1, ...more)
// step 2: (...)(2)
// args = [1, 2], 2 >= 3 is false
// -> returns (...more) => curried(1, 2, ...more)
// step 3: (...)(3)
// args = [1, 2, 3], 3 >= 3 is TRUE
// -> returns fn(1, 2, 3) === 6
console.log(curry((a, b, c) => a + b + c)(1)(2)(3)); // 6
Each call that does not yet have enough arguments returns another gathering function; the call that finally reaches the threshold invokes the real fn. That is the whole mechanism. Read it twice and it will never look like magic again. ;-)
A practical, readable use
Currying really shines with small "accessor" style functions - the kind you feed to map, filter, and friends (episode 18). Here is a curried property getter that produces reusable accessors:
// a curried "prop getter"
const prop = (key) => (obj) => obj[key];
const getName = prop("name");
const users = [{ name: "scipio" }, { name: "alice" }];
console.log(users.map(getName)); // ["scipio", "alice"] - clean and reusable
That prop("name") builds a getName function once, and then map applies it across the whole array. Compare it to writing users.map(u => u.name) inline - for a single use the inline arrow is fine, but the moment you need getName in three places, the curried version pays off by giving you a named, reusable thing. This is exactly the kind of spot where currying is not showing off; it is genuinely tidier.
How other languages handle this
Since quite a few of you arrived here from the Learn Python Series (with some Rust and Go readers along for the ride), a look sideways is illuminating, because JavaScript sits in an interesting middle ground on this topic.
Haskell - the language named after the very Curry we mentioned - curries everything by default. In Haskell there is no such thing as a two-argument function; a function of "two arguments" is really a function that takes one argument and returns a function taking the next. Partial application is therefore free and pervasive: you just supply fewer arguments than the "full" count and you get a specialized function back. JavaScript has to emulate with closures what Haskell gives you as the ground truth of the language.
Python has no built-in currying, but its standard library ships functools.partial, which is partial application made official. It reads very naturally:
from functools import partial
def multiply(a, b):
return a * b
double = partial(multiply, 2) # fix the first argument
print(double(5)) # 10
That partial(multiply, 2) is the Python twin of our JavaScript partial helper and of multiply.bind(null, 2) - same idea, different spelling. Python programmers reach for functools.partial exactly where a JS programmer reaches for bind or a curried helper.
Rust does not curry either (its functions have a fixed arity), but closures capture their environment and give you the same specialization, much like our hand-rolled JS version:
fn main() {
let multiply = |a: i32, b: i32| a * b;
let double = move |b: i32| multiply(2, b); // closure fixes the first arg
println!("{}", double(5)); // 10
}
The move closure captures multiply and pins 2 in place - conceptually identical to what our JavaScript closure does. Go is even more spartan: no currying, no partial in the standard library, but you can return a closure from a function to get partial application by hand, the same way we did at the top of this episode. The pattern travels; only the syntax changes.
The takeaway: currying-as-a-default is a functional-language idea (Haskell, ML), while most mainstream languages - JavaScript very much included - reach the same practical benefits through closures and partial application. Understanding the closure underneath means you can reproduce this technique in almost any language you meet.
When to use these
Now for the honest part, because I would be doing you a disservice to sell these as always-good. Currying and heavy partial application are powerful in a functional style, and they read beautifully when you are building pipelines of small, composable functions (Phase 10 leans on them hard). But over-currying ordinary, everyday code makes it cryptic - f(a)(b)(c) is unfamiliar and faintly alien to many readers, and a wall of it will slow a teammate down rather than help them. Use these techniques where they genuinely earn their place:
- To create specialized functions from general ones (
sayHifromcurriedGreet,infofromlog). - To make functions plug cleanly into composition pipelines (next episode's whole topic).
- To configure some behaviour once and reuse it many times without repeating yourself.
For plain, one-off calls, a normal multi-argument function is clearer, full stop. multiply(2, 5) beats curry(multiply)(2)(5) every time when you only need it once. Like every tool in this series, currying is excellent in its place and pure noise everywhere else. Aim for clarity, not cleverness - the reader you are helping most is usually yourself, six months from now, trying to remember what on earth g(x)(y)(z) was supposed to do.
Try it yourself
Three exercises, increasing in difficulty. Try to predict each result before you run it. Full solutions open the next episode.
- Write a curried function
curriedMultiply(a)(b)(c)that multiplies three numbers, using nested functions. Then create atimesSix = curriedMultiply(2)(3)intermediate and call it with several different values. - Implement the generic
curry(fn)helper from this episode and use it on a functionvolume(l, w, h). Show thatcurry(volume)(2)(3)(4),curry(volume)(2, 3)(4), andcurry(volume)(2, 3, 4)all give the same answer. - Write a curried
prop(key)(obj)getter and usemapto extract thecityfrom an array of user objects. Then explain, in one sentence, the difference between currying (one argument per call) and partial application (fixing some arguments at once).
So what did we actually cover?
- Partial application fixes some of a function's arguments now, returning a function that takes the rest (which is precisely what
bindfrom episode 23 does), all powered by closures. - Currying transforms an N-argument function into a chain of N single-argument functions, each returning the next until all arguments are gathered, then computing.
- The difference: partial application fixes some args and takes the rest at once; currying takes exactly one arg per call, uniformly.
- Currying's real value is that each intermediate stage is a reusable, specialized function - ideal for configuration (
info,warn,error) and for composition. - A generic
curry(fn)helper gathers arguments (usingfn.length, rest/spread, and recursion) until it has enough, then calls the original, accepting any grouping of calls. - Other languages reach the same benefits differently: Haskell curries by default, Python has
functools.partial, Rust and Go use closures - the closure underneath is the portable idea. - Use these where they add clarity (specialization, pipelines); avoid over-currying ordinary code.
Next episode we put these curried, single-argument functions to work with function composition, building data pipelines by wiring small functions together into powerful ones - and you will see why currying and composition are such natural partners.