Back to issues
JAVASCRIPT

Currying in JavaScript: When It Makes Sense (and When It's Over-Engineering)

Currying transforms multi-argument functions into chains of single-argument functions. Learn when this technique is powerful and when it's overkill.

By Thiago Saraiva5 MIN

Have you ever seen code like this: createLogger('INFO')('App')('Server started') and thought "what the hell is that"? That's currying, and when used well, it's one of the most elegant functional programming techniques. The problem is that many people reach for it without need and turn simple code into something unreadable.

Let's demystify it.

What Is Currying?

Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument:

The magic isn't in calling f(1)(2)(3) instead of f(1, 2, 3). The magic is being able to call f(1) and save the result for later.

Mental model: a regular function is a bulk delivery. You hand over all the boxes at once, you get the result. Currying is a nested gift. You unwrap one box, find another inside, unwrap that, and so on. Each unwrap remembers what was in the previous box (closure), so by the time you open the last one, every argument you ever passed is still there waiting.

Generic Curry: The Utility Function

You don't need to manually write each curried function. A helper handles it:

Currying vs Partial Application

This is where almost everyone trips. They look similar, they're not the same.

Currying transforms f(a, b, c) into a chain where each step takes exactly one argument. It's a structural transformation of the function itself.

Partial application takes a function and pre-fills some of its arguments, returning a new function that expects the rest. No rule about how many at a time.

Rule of thumb: every curried function enables partial application, but not every partial application is currying.

Where Currying Really Shines

Configurable Logger

The classic example, and for good reason. You configure once and reuse:

You build a specialized function out of a generic one. That's composition.

Composable Validators

Transformation Pipelines

Currying Is Already in Code You Use

You've probably shipped curried code without noticing:

  • Ramda and lodash/fp: functions are auto-curried by default, and arguments are arranged data-last. R.map(fn)(list) and R.map(fn, list) both work.
  • Redux Thunk: a thunk is dispatch => getState => { ... }, a curried signature that lets the middleware inject dispatch first and hand you getState on demand.
  • styled-components: styled.div`...` returns a component that receives props, then renders. Tagged templates plus a curried-style API under the hood.
  • React useCallback / useMemo: the factory pattern (id) => useCallback(() => handle(id), [id]) is currying to bind an argument into the closure.
  • Express-style middleware factories: authorize('admin')(req, res, next) configures the check once, applies it many times.

When You Should NOT Curry

Currying is a tool. Sometimes it's the wrong one:

  • Object methods that rely on this. user.save curried loses its receiver. Either use .bind() or keep it plain.
  • Hot loops and perf-critical paths. Each curry level creates a closure and an extra call. Tight inner loops (thousands of iterations per frame, game engines, heavy data crunching) pay a measurable cost.
  • Teams unfamiliar with FP. If the reviewer has to Google a => b => c => ..., you're creating friction, not elegance.
  • Functions you'll always call with every argument at once. If f(a, b, c) never becomes f(a) saved somewhere, currying is cosplay.
  • Variadic functions (...args). The generic curry relies on fn.length, which ignores rest params. It'll never resolve.

The Pitfalls of Currying

  1. Currying is not partial application. Currying applies one argument at a time. Partial application can apply several.
  2. fn.length doesn't count rest params. If your function uses ...args, the generic curry breaks.
  3. Debugging gets harder. Stack traces with nested closures read cryptic.
  4. Loss of this. Object methods don't play well with curry. Use .bind() if needed.
  5. Over-engineering. Not every function needs to be curried. If you're never going to partially apply, don't curry.

FAQ

How do I type a curried function in TypeScript? For fixed arity, just chain: (a: number) => (b: string) => boolean. For a generic curry helper, you need recursive conditional types (Curry<F> utilities exist in libraries like ts-toolbelt). Most teams type the specific curried signature by hand, it's clearer.

Are there good autocurry libraries? Yes. Ramda and lodash/fp curry everything by default. Rambda is a lighter, faster alternative with a smaller bundle and a near-identical API. For one-off needs, a 5-line curry helper (like the one above) beats pulling in a dependency.

What's the performance overhead? Measurable but usually negligible. Each extra call creates a closure and a function invocation. For normal app code, invisible. For hot loops doing millions of ops, benchmark before currying.

Is an arrow chain (a) => (b) => ... really currying? Yes, that's manual currying. It's the most explicit form. The curry(fn) helper just automates writing that chain and lets you pass multiple args per call.

How does currying differ from bind()? bind() does partial application (you can pre-fill any number of args) and binds this. Currying creates a new argument-by-argument structure and doesn't touch this. Overlapping problems, different solutions.

Key Takeaways

Currying is powerful for building specialized functions out of generic ones: configurable loggers, reusable validators, transformation pipelines. But it's a tool, not a dogma.

If you write f(a)(b)(c) and never use f(a) separately, you're adding complexity with no gain. Curry when partial application is the actual goal, not as decoration.