Currying in JavaScript: Why Closures Beat `.bind()` Every Time

You've seen the code. A function that returns a function that returns a function. You squinted at it, maybe copy pasted it into your project, and quietly moved on. Later, someone asks you to explain currying in a code review and you freeze.
Or maybe you've been using .bind() for partial application for years and it always felt a little... off. Like you were forcing a screwdriver to work as a hammer.
This post is the explanation you wanted the first time. By the end, these patterns won't just work — they'll make sense.
First: What problem are we actually solving?
The core idea is simple: sometimes you have a function that takes multiple arguments, but you only know some of those arguments right now. You want to pre-fill what you know and call the rest later.
Think of it like a pizza order form. You always order from the same place (fixed), but the toppings change (variable). You don't want to re-enter the restaurant details every time. You want a specialized version of order() that already knows where it's going.
That's partial application. Currying takes it further: it transforms a function so each argument gets its own call — f(a, b, c) becomes f(a)(b)(c).
There are two main ways to do this in JavaScript: .bind() and closures. They're not the same, and one is vastly more powerful.
Approach 1: .bind() — Quick but limited
.bind() creates a new function with some arguments pre-filled from the left.
function add(a, b, c) {
return a + b + c;
}
const add5 = add.bind(null, 5);
console.log(add5(10, 20)); // 35
const add5and10 = add.bind(null, 5, 10);
console.log(add5and10(20)); // 35
Clean. Readable. Gets the job done for fixed-arity functions (functions with a known, fixed number of arguments).
But notice that null as the first argument. That's the this context — bind was designed to rebind this, and partial application is kind of a side effect of that design. You're using a this-binding tool for an argument-filling job.
That conceptual mismatch has practical consequences:
- Fixed argument count only.
.bind()has no mechanism to accumulate arguments dynamically. You pre-fill from the left and that's it. - No dynamic currying. You can't build a general purpose curry utility around
.bind(). - Changes
this. Sometimes you want that. Often you don't — but now you have to think about it.

Approach 2: Closures — The real foundation
A closure is just a function that remembers the variables from the scope it was defined in, even after that outer scope has returned.
That "memory" is exactly what makes currying work.
function add(a) {
return function(b) {
return function(c) {
return a + b + c; // a and b are remembered via closure
};
};
}
console.log(add(1)(2)(3)); // 6
Each inner function has access to the arguments passed to every outer function. a doesn't disappear when add(1) returns — the inner function holds a reference to it.
Think of it like a backpack. Every time you go one level deeper, you hand the next function a backpack with all the previous values. It carries them until you're ready to use them.
Where closures really shine: variable argument counts
Here's where .bind() simply can't compete. What if you don't know how many arguments you'll get?
function sum(a) {
return function(b) {
if (b === undefined) {
return a; // done collecting — return the total
}
return sum(a + b); // keep accumulating
};
}
console.log(sum(1)(2)(3)(4)()); // 10
Walk through this:
sum(1)returns a function, closing overa = 1sum(1)(2)calls that function withb = 2, which returnssum(3)sum(3)(3)returnssum(6)sum(6)(4)returnssum(10)sum(10)()—bisundefined, so we return10The closure keeps state alive between calls. No class, no external variable, no mutation. Just functions remembering their past.

A practical curry utility
The real payoff: you can write a general-purpose curry() function that works with any fixed-arity function.
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
// We have all the arguments fn expects — call it
return fn(...args);
}
// Not enough arguments yet — return a function that collects more
return function(...next) {
return curried(...args, ...next);
};
};
}
The key insight: fn.length tells you how many parameters fn expects. The closure (curried) accumulates arguments across calls until that threshold is met.
function multiply(a, b, c) {
return a * b * c;
}
const curriedMul = curry(multiply);
console.log(curriedMul(2)(3)(4)); // 24
console.log(curriedMul(2, 3)(4)); // 24 — batch arguments work too
console.log(curriedMul(2)(3, 4)); // 24
All three produce 24. The callers get to decide how they group the arguments — the curry utility doesn't care.
This flexibility is impossible with .bind(). You can't batch arguments and you can't build this kind of accumulation logic around it.
A quick terminology check: currying vs partial application
People use these terms interchangeably, which causes endless confusion. They're related but distinct:
Partial application — pre-filling some arguments of a function, getting back a function that takes the rest.
// Partial application
const add10 = (b, c) => add(10, b, c);
// or with bind:
const add10 = add.bind(null, 10);
Currying — transforming f(a, b, c) into f(a)(b)(c). Each call takes exactly one argument and returns a new function.
Closures are the foundation of real currying in JavaScript. .bind() can do partial application, but it can't do currying in the general sense.
When .bind() still wins
To be fair: there's one thing .bind() does that closures don't automatically replicate — permanently rebinding this.
const obj = {
x: 10,
getX() {
return this.x;
}
};
const fn = obj.getX;
console.log(fn()); // undefined — lost `this`
const bound = obj.getX.bind(obj);
console.log(bound()); // 10 — this is permanently locked
A closure won't save you here without explicitly capturing the reference:
const bound = () => obj.getX(); // works, but different semantics
Arrow functions preserve the lexical this (the this from where they're defined), which handles some use cases — but it's not the same as .bind()'s hard binding.
So .bind() has a rightful home. It's just a narrow one.

The practical cheat sheet
Use closures when:
- You need real currying (chained single-argument calls)
- You need flexible partial application
- You're doing functional programming (pipelines, composition)
- You need to accumulate a variable number of arguments
- You're writing utility functions like
curry(),compose(), ormemoize()Use.bind()when: - You need to permanently fix
thisfor a callback - You want a quick, one-off partial application for a simple fixed-arity function
- You're passing a method as a callback and need it to keep its context
Three things to take away
- Closures are how currying actually works in JavaScript. Each inner function holds a reference to outer variables — that's the "memory" that makes argument accumulation possible.
.bind()is athis-binding tool that happens to support partial application. It's useful for that exact scenario, but it can't build flexible curry utilities or handle dynamic argument counts.- Currying and partial application are different things. Partial application pre-fills some args. Currying transforms a multi-argument function into a chain of single-argument functions. Closures can do both;
.bind()can only do partial application. Next time you seefn => arg => anotherArg => ..., you'll know exactly what's happening: a chain of closures, each one carrying the accumulated context forward. No magic — just functions remembering their past.



