Currying and Partial Application

· 7 min read · Updated March 29, 2026 · intermediate
javascript functional-programming

Currying and partial application are two related techniques that change how you think about function arguments. Instead of passing all arguments at once, you break a function’s arity into smaller, reusable pieces. If you’ve ever written something like const doubleAll = arr => arr.map(x => x * 2), you’ve already glimpsed why this matters — you’re separating the transformation from the data it operates on.

Closures: The Mechanism Behind Currying

Currying relies on closures. A closure is when a function retains access to variables from its outer scope even after that outer function has returned. Without closures, currying wouldn’t work.

function outer(a) {
  return function inner(b) {
    return a + b  // 'a' is captured from outer's scope
  }
}
const fn = outer(5)
fn(3)  // 8 — 'a' (5) is still accessible inside 'inner'

Every time you call a curried function, it returns a new function that closes over the arguments you’ve already provided. This is what allows the chained f(a)(b)(c) pattern to work.

What is Currying?

Currying transforms a function that takes multiple arguments into a sequence of functions, each taking exactly one argument. Named after mathematician Haskell Curry, it forces a strict unary chain.

// Regular function: all args at once
function add(a, b, c) {
  return a + b + c
}
add(1, 2, 3)  // 6

// Curried version: one arg per call
const curriedAdd = a => b => c => a + b + c
curriedAdd(1)(2)(3)  // 6

Think of it like ordering a pizza one topping at a time. You place the base, then cheese, then pepperoni — each step building on the last. You can’t skip ahead, but you also can’t mess up the order.

Writing a curry() Helper

You don’t need a library to curry functions. Here’s a working implementation:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    }
    return function(...args2) {
      return curried.apply(this, args.concat(args2))
    }
  }
}

The key is fn.length, which tells you how many parameters a function expects. When you’ve collected enough arguments, the original function runs. Until then, the curried function keeps accumulating arguments.

function multiply(a, b, c) {
  return a * b * c
}
const curriedMultiply = curry(multiply)

curriedMultiply(2)(3)(4)    // 24
curriedMultiply(2, 3)(4)   // 24 — mixed style works too
curriedMultiply(2)(3, 4)   // 24

Most implementations support passing some arguments upfront and the rest later, which is more ergonomic than requiring purely unary calls.

Arrow Function Currying

In modern JavaScript, you often write curried functions directly with arrow functions — no helper needed:

const add = a => b => c => a + b + c
const multiply = a => b => c => a * b * c

add(1)(2)(3)       // 6
multiply(2)(3)(4)  // 24

This compact syntax is common in functional codebases. Libraries like Ramda use this style throughout their APIs. You lose named parameters, but you gain the ability to specialize functions on the left side of a composition.

Partial Application with bind

Partial application fixes some arguments of a function to produce a new function with fewer parameters. It’s different from currying — you can fix any subset of arguments at any time, not just the first one.

function greet(greeting, name) {
  return `${greeting}, ${name}!`
}

const sayHello = greet.bind(null, 'Hello')
sayHello('Alice')   // "Hello, Alice!"
sayHello('Bob')     // "Hello, Bob!"

bind returns a new function with the first argument set to the context (ignored here since greet doesn’t use this) and any additional arguments pre-filled. You can pre-fill multiple arguments:

const greetAlice = greet.bind(null, 'Hello', 'Alice')
greetAlice()  // "Hello, Alice!"

In modern code, undefined is preferred over null for the context since we’re not using this in pure functions.

Lodash _.curry and _.curryRight

For production code, reach for a battle-tested implementation. Lodash’s curry handles edge cases and supports placeholders for fine-grained control:

import { curry, curryRight } from 'lodash'

const divide = (a, b) => a / b
const curriedDivide = curry(divide)

curriedDivide(10)(2)   // 5
curriedDivide(10, 2)   // 5 — also works

// curryRight fills arguments from the right
const curriedDivideRight = curryRight(divide)
curriedDivideRight(2)(10)  // 5

Lodash also supports placeholders (_) when you need to partially apply an argument in the middle:

const greet = (greeting, name, punct) => `${greeting}, ${name}${punct}`
const partial = curry(greet)
partial('Hello', _, '!')('Alice')  // "Hello, Alice!"

Practical Patterns

API Client Factories

Currying shines when building layers of configuration:

const createClient = baseUrl => endpoint => headers => {
  return fetch(`${baseUrl}/${endpoint}`, { headers })
}

const githubClient = createClient('https://api.github.com')
const fetchRepos = githubClient('repos')
const withAuth = headers => fetchRepos(headers)

// Later, with different auth:
fetchRepos({ authorization: 'token abc' })

Each call in the chain narrows the scope — you establish the base URL once, then create specialized endpoint functions, and finally provide headers per request.

Event Handlers with Preset Config

Attaching handlers with pre-filled data is straightforward:

const handleClick = buttonId => event => {
  console.log(`Button ${buttonId} clicked at`, event.clientX, event.clientY)
}

document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('click', handleClick(btn.id))
})

handleClick(btn.id) returns a function that closes over btn.id. When the click fires, the handler has access to the correct button ID without any extra closure wiring on your part.

Composing Data Transformations

Pair curried map and filter with a pipe function for expressive data processing:

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x)

const double = x => x * 2
const isEven = x => x % 2 === 0

const doubleEvens = pipe(
  arr => arr.filter(isEven),
  arr => arr.map(double)
)
doubleEvens([1, 2, 3, 4])  // [4, 8]

The curried style means filter and map take their function argument first and their array second. This ordering is what makes point-free composition possible — each step is a function waiting for data.

Currying vs Partial Application

The terms get mixed up, but there’s a real distinction:

AspectCurryingPartial Application
OutputAlways unary chainFixes any number of args
ArgumentsOne at a timeAny number at a time
Patternf(a)(b)(c)f(a, b)(c)
Library support_.curry, R.curry_.partial, R.flip, bind
// Currying: unary only
const add = a => b => c => a + b + c
add(1)(2)(3)  // 6

// Partial application: fix any subset
const add = (a, b, c) => a + b + c
const fixFirst = a => (b, c) => add(a, b, c)
fixFirst(1)(2, 3)  // 6

Currying is a specific transformation with a fixed arity of one. Partial application is more general — you decide how many arguments to fix upfront.

Common Pitfalls

Performance in hot paths. Every curried call allocates a new function object. In tight loops, this garbage collection pressure adds up. Profile before committing to curried patterns in performance-critical code.

// Allocation in a loop — watch out
const curriedAdd = a => b => a + b
for (let i = 0; i < 100000; i++) {
  process(curriedAdd(1)(i))  // Creates 100k function objects per iteration
}

Readability for unfamiliar audiences. The a => b => a + b style reads differently from what most JavaScript developers expect. In a mixed-ability team, clarity often beats elegance.

Harder debugging. Each curried layer adds a frame to stack traces. When something goes wrong in a composed chain of curried functions, the error messages can be opaque. Name your intermediate steps if you need to debug.

Conclusion

Currying and partial application are tools for reshaping functions. Currying forces a unary chain — each call takes exactly one argument until the original function runs. Partial application is more flexible: fix whatever arguments you want upfront, pass the rest later.

Both patterns shine when you need reusable, specialized functions. Event handlers, API clients, and data pipelines all benefit. The real win is composition — when small functions snap together cleanly, the overall structure reads like the problem domain rather than the implementation details.

Closures are the mechanism that makes currying possible. Function composition chains curried pieces together. Scope determines what each returned function can access. Understanding these underlying mechanics gives you the confidence to use these patterns without surprises.

See Also