What is currying in JavaScript
Currying is a technique that transforms a function taking multiple arguments into a chain of single-argument functions, where each call returns the next function until all arguments are collected and the original logic runs.
Theory
TL;DR
- Analogy: like a coffee order step by step - choose size (returns milk options), then milk (returns syrup), instead of saying everything at once
- Each curried call returns a new function that closes over the previous argument
- Regular:
sum(1, 2, 3). Curried:sum(1)(2)(3). Same result, different shape - Use it when you reuse fixed args (base URL, user role, log level). Skip it for one-off calls
- Partial application falls out naturally: stop halfway, save the result, reuse it
Quick example
// Regular - all args at once
function regularSum(a, b, c) { return a + b + c; }
regularSum(1, 2, 3); // 6
// Curried - one arg at a time
const currySum = a => b => c => a + b + c;
currySum(1)(2)(3); // 6
// Partial application: fix one arg, reuse the rest
const add5 = currySum(5);
add5(3); // 8
add5(10); // 15Each call returns a function that holds the previous argument in its closure. The final call runs the logic with everything it needs.
Key difference
A regular function takes all its arguments at once and either runs or produces NaN/undefined if something is missing. A curried function accepts them one at a time, so you can stop halfway, store the partially-applied result, and reuse it across your codebase. That stored intermediate is called a partial application.
When to use
- Reusing a fixed argument across many calls (API base URL, user role in auth middleware) - curry it, create specialized versions once
- Building function pipelines where each step takes one input
- Config-heavy utilities like loggers where the level is set once and reused
- One-shot math or a simple transform with no reuse - plain function is cleaner
How JavaScript handles currying
V8 creates a new closure on each curried call. Each closure captures prior arguments in its lexical scope, no global state. When the final argument arrives, the engine reconstructs the full argument list and calls the original function. Short closure chains get inlined by V8. Longer ones may allocate on the heap, so profile before currying inside tight loops.
Universal curry utility
Nesting functions manually gets old fast. A universal curry wrapper checks how many arguments the function expects (fn.length) and keeps collecting until it has enough:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn(...args);
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
const max = (a, b) => Math.max(a, b);
const curriedMax = curry(max);
curriedMax(10)(20); // 20 - one at a time
curriedMax(10, 30); // 30 - both at once also worksYou can pass args one at a time or in groups. The base case fires as soon as the total reaches fn.length.
Common mistakes
Missing return in the outer function:
// Wrong
function bad(a) {
function inner(b) { return a + b; } // outer never returns inner
}
bad(1)(2); // TypeError: bad(...) is not a functionThe outer must explicitly return the inner. Arrow functions make this harder to miss: a => b => a + b.
Assuming JavaScript auto-curries:
const add = (a, b) => a + b;
const add1 = add(1); // NaN - b becomes undefined, not a new functionNative functions don't curry themselves. Wrap with a curry utility or write manually.
Missing the base case in a custom curry:
// Wrong - infinite recursion
function badCurry(fn) {
return function(...args) {
return badCurry(fn)(...args); // no exit condition, stack overflow
};
}Always check args.length >= fn.length before recursing.
Currying a variadic function:
const sum = (...args) => args.reduce((a, b) => a + b, 0);
const curriedSum = curry(sum); // fn.length is 0, base case fires immediately
curriedSum(1, 2, 3); // works, but currying has no effectfn.length returns 0 for rest parameters. Pass the expected arity explicitly if you need it.
Real-world usage
In most codebases, currying first shows up not in a functional programming tutorial but in an auth middleware factory someone wrote on day one and never changed.
- Lodash:
_.currywraps any function for composable chains -curry(map)(double)(data) - Ramda: all functions curried by default, point-free style via
R.pipe - Express:
requireRole('admin')middleware factories are the most common real-world pattern - Redux-Observable: curried operators like
switchMapfor composing epics - React fetch hooks:
const getUser = curryFetch('/api/users')fixes the base URL once
Follow-up questions
Q: What is the difference between currying and partial application?
A: Partial application fixes some arguments and returns a function expecting the rest. Currying always breaks into single-argument steps. Every curried function supports partial application, but you don't need currying to partially apply.
Q: Implement a curried version of Array.prototype.map.
A: const curryMap = fn => arr => arr.map(fn). Use as curryMap(x => x * 2)([1, 2, 3]), which returns [2, 4, 6].
Q: How does currying enable function composition?
A: Curried functions take one input and return one output, which is exactly what compose and pipe expect. You can build pipe(trim, toLower, greet) without wrapper functions around each step.
Q: What happens when you curry a function with default parameters?
A: Default parameters don't count in fn.length. function add(a, b = 0) has fn.length === 1. The universal curry fires after just one argument, treating b as always defaulting to 0.
Q: How does V8 optimize curried closures at the engine level? (Senior)
A: V8 escape analysis avoids heap allocation for short-lived closures. Small curried chains get inlined. Hot paths generating many closures may deoptimize - check with --trace-ic. For performance-critical code, .bind() can be faster than manual currying because it is a native operation.
Examples
Basic: reusable adder
const add = a => b => a + b;
const add10 = add(10); // fix 10, get a reusable adder
add10(5); // 15
add10(20); // 30
add10(-3); // 7add10 is a plain function that adds 10. Create it once, use it anywhere, without touching the original add.
Intermediate: Express role-based middleware
const requireRole = role => (req, res, next) => {
if (req.user?.role === role) return next();
res.status(403).send('Access denied');
};
const adminOnly = requireRole('admin');
const editorOnly = requireRole('editor');
app.get('/admin/dashboard', adminOnly, dashboardHandler);
app.get('/blog/edit', editorOnly, editHandler);
// req.user.role === 'admin' -> calls next()
// anything else -> 403Without currying, you'd pass the role at every route definition or duplicate the check. This way the logic lives once and you name the specialized versions.
Advanced: universal curry with a logger
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn(...args);
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
function formatMessage(level, timestamp, message) {
return `[${level}] ${timestamp}: ${message}`;
}
const log = curry(formatMessage);
const logError = log('ERROR'); // fix level
const logErrorNow = logError(new Date().toISOString()); // fix timestamp
logErrorNow('Database connection failed');
// [ERROR] 2024-01-15T10:30:00.000Z: Database connection failed
logErrorNow('Timeout on /api/users');
// [ERROR] 2024-01-15T10:30:00.000Z: Timeout on /api/users
log('INFO', new Date().toISOString(), 'Server started'); // all at once also worksThe base case (args.length >= fn.length) is what makes passing args in groups work alongside one-at-a-time calls.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.