Skip to main content

How to Use Currying in JavaScript

Currying is one of those concepts that sounds intimidating at first but, once understood, becomes a natural tool in your JavaScript toolkit. It transforms how you think about functions, letting you build small, reusable pieces that snap together like building blocks.

Rooted in functional programming and named after mathematician Haskell Curry, currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument. Instead of calling f(a, b, c), you call f(a)(b)(c).

This guide explains what currying is, how to implement it from scratch, how it differs from partial application, and where it shines in real-world JavaScript code.

What Is Currying?

Currying transforms a function with multiple parameters into a chain of functions, each accepting one argument at a time.

A regular function that takes three arguments:

function add(a, b, c) {
return a + b + c;
}

add(1, 2, 3); // 6

The curried version of the same function:

function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}

curriedAdd(1)(2)(3); // 6

Each call returns a new function that "remembers" the previously passed argument through closures. The computation only happens when all arguments have been provided.

With arrow functions, the curried version becomes much more concise:

const curriedAdd = a => b => c => a + b + c;

curriedAdd(1)(2)(3); // 6

Step-by-Step Breakdown

Let's trace exactly what happens with curriedAdd(1)(2)(3):

const curriedAdd = a => b => c => a + b + c;

// Step 1: curriedAdd(1) returns a function that remembers a = 1
const step1 = curriedAdd(1); // b => c => 1 + b + c

// Step 2: step1(2) returns a function that remembers a = 1, b = 2
const step2 = step1(2); // c => 1 + 2 + c

// Step 3: step2(3) finally computes the result
const result = step2(3); // 1 + 2 + 3 = 6

console.log(result); // 6

The power here is not in the final call. It is in the intermediate functions. Each step produces a specialized, reusable function:

const addTen = curriedAdd(10);    // b => c => 10 + b + c
const addTenAndFive = addTen(5); // c => 10 + 5 + c

console.log(addTenAndFive(1)); // 16
console.log(addTenAndFive(20)); // 35
console.log(addTen(0)(0)); // 10

Implementing Curry: Simple and Advanced

Manually currying every function by hand is tedious. What we really want is a curry helper that transforms any regular function into a curried one automatically.

Simple Implementation (Fixed Arity)

The simplest curry function works with functions that have a known number of parameters:

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

Let's break down what this does:

  1. It checks if enough arguments have been collected (args.length >= fn.length)
  2. If yes, it calls the original function with all collected arguments
  3. If no, it returns a new function that waits for more arguments and concatenates them
function multiply(a, b, c) {
return a * b * c;
}

const curriedMultiply = curry(multiply);

// All of these produce the same result:
console.log(curriedMultiply(2)(3)(4)); // 24
console.log(curriedMultiply(2, 3)(4)); // 24
console.log(curriedMultiply(2)(3, 4)); // 24
console.log(curriedMultiply(2, 3, 4)); // 24

Notice that this implementation is flexible: you can pass one argument at a time, or pass several at once. This makes it much more practical than strict one-argument-at-a-time currying.

How fn.length Works

The implementation relies on fn.length, which returns the number of parameters a function declares:

function one(a) {}
function two(a, b) {}
function three(a, b, c) {}

console.log(one.length); // 1
console.log(two.length); // 2
console.log(three.length); // 3
caution

fn.length does not count rest parameters or parameters with default values:

function example(a, b = 10, ...rest) {}
console.log(example.length); // 1 (only counts `a`)

This means the simple curry implementation will not work correctly with functions that use default parameters or rest parameters.

Advanced Implementation (Preserving Context)

The simple version works for most cases. Here is a more robust version that handles edge cases:

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

// Create a bound function that preserves `this`
const partial = (...nextArgs) => {
return curried.apply(this, [...args, ...nextArgs]);
};

// Preserve the remaining arity for inspection
Object.defineProperty(partial, 'length', {
value: fn.length - args.length
});

return partial;
};
}

Testing with object methods:

const calculator = {
multiplier: 10,
compute: curry(function(a, b) {
return (a + b) * this.multiplier;
})
};

console.log(calculator.compute(2)(3)); // 50 - i.e. (2 + 3) * 10

An Even Simpler Approach for Two Arguments

If you frequently curry functions with exactly two parameters, a dedicated helper is clean and efficient:

const curry2 = fn => a => b => fn(a, b);

const pow = curry2(Math.pow);
const square = pow(2);

// Note: Math.pow(base, exponent), so pow(2) creates base=2
// This means square(n) computes 2^n, not n^2
console.log(square(3)); // 8 (2^3)
console.log(square(10)); // 1024 (2^10)

Partial Application vs. Currying

Currying and partial application are closely related but not the same thing. They are often confused, so let's clarify the difference.

Currying

Currying transforms a function of N arguments into N nested functions of one argument each. The transformation is about the structure of the function:

// Original: f(a, b, c)
// Curried: f(a)(b)(c)

const curriedSum = a => b => c => a + b + c;
curriedSum(1)(2)(3); // 6

Partial Application

Partial application fixes some arguments of a function and returns a new function that takes the remaining arguments. The result is a function with fewer parameters:

function sum(a, b, c) {
return a + b + c;
}

// Partially apply the first argument
function partialSum(b, c) {
return sum(10, b, c); // `a` is fixed to 10
}

partialSum(2, 3); // 15

Using Function.prototype.bind() for partial application:

function sum(a, b, c) {
return a + b + c;
}

const addTen = sum.bind(null, 10); // fixes a = 10
console.log(addTen(2, 3)); // 15

const addTenAndFive = sum.bind(null, 10, 5); // fixes a = 10, b = 5
console.log(addTenAndFive(3)); // 18

Side-by-Side Comparison

function volume(length, width, height) {
return length * width * height;
}

// --- Currying ---
const curriedVolume = curry(volume);
curriedVolume(2)(3)(4); // 24
curriedVolume(2)(3); // returns a function waiting for height
curriedVolume(2); // returns a function waiting for width and height

// --- Partial Application ---
const boxVolume = volume.bind(null, 2); // length fixed to 2
boxVolume(3, 4); // 24

const flatBoxVolume = volume.bind(null, 2, 3); // length=2, width=3 fixed
flatBoxVolume(4); // 24
FeatureCurryingPartial Application
What it doesTransforms function structureFixes specific arguments
Arguments per callOne at a time (strict) or flexibleRemaining unfixed arguments
ReturnsChain of single-argument functionsOne function with fewer parameters
ReusabilityEvery intermediate step is reusableCreates one specialized function
info

In practice, the curry helper we built earlier blends both concepts. It supports currying (one argument at a time) and partial application (multiple arguments at once). This is intentional and matches how libraries like Lodash implement it.

A Generic Partial Application Helper

While bind works for partial application, you can build a dedicated helper:

function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}

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

const greetHello = partial(greet, "Hello", "!");
console.log(greetHello("Alice")); // "Hello, Alice!"
console.log(greetHello("Bob")); // "Hello, Bob!"

const greetHi = partial(greet, "Hi", ".");
console.log(greetHi("Charlie")); // "Hi, Charlie."

Practical Uses

Currying shines in several real-world scenarios. Let's explore the most common ones.

Reusable Utility Functions

Create specialized versions of general functions:

const curry = fn => function curried(...args) {
return args.length >= fn.length
? fn(...args)
: (...next) => curried(...args, ...next);
};

// A general-purpose filter
const filter = curry((predicate, array) => array.filter(predicate));

// Create specialized filters
const getEvenNumbers = filter(n => n % 2 === 0);
const getPositive = filter(n => n > 0);
const getLongStrings = filter(s => s.length > 5);

console.log(getEvenNumbers([1, 2, 3, 4, 5, 6])); // [2, 4, 6]
console.log(getPositive([-2, -1, 0, 1, 2])); // [1, 2]
console.log(getLongStrings(["hi", "hello", "wonderful"])); // ["wonderful"]

Composing Data Transformations

Curried functions are perfect for building data pipelines:

const map = curry((fn, array) => array.map(fn));
const filter = curry((predicate, array) => array.filter(predicate));
const reduce = curry((fn, initial, array) => array.reduce(fn, initial));
const prop = curry((key, obj) => obj[key]);
const pipe = (...fns) => input => fns.reduce((acc, fn) => fn(acc), input);

const users = [
{ name: "Alice", age: 25, active: true },
{ name: "Bob", age: 30, active: false },
{ name: "Charlie", age: 35, active: true },
{ name: "Diana", age: 28, active: true }
];

// Build a pipeline: get names of active users over 26
const getActiveAdultNames = pipe(
filter(user => user.active),
filter(user => user.age > 26),
map(prop("name"))
);

console.log(getActiveAdultNames(users)); // ["Charlie", "Diana"]

Each piece of this pipeline is a small, testable, reusable function. Currying is what makes filter(user => user.active) and map(prop("name")) possible as standalone expressions.

Configuration and Factory Functions

Currying naturally creates configurable functions where you set up options first and data later:

const createLogger = curry((level, prefix, message) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] [${prefix}] ${message}`);
});

// Create specialized loggers
const errorLog = createLogger("ERROR");
const appError = errorLog("APP");
const dbError = errorLog("DATABASE");

const infoLog = createLogger("INFO");
const appInfo = infoLog("APP");

appError("Connection refused");
// [2024-01-15T10:30:00.000Z] [ERROR] [APP] Connection refused

dbError("Query timeout");
// [2024-01-15T10:30:00.000Z] [ERROR] [DATABASE] Query timeout

appInfo("Server started");
// [2024-01-15T10:30:00.000Z] [INFO] [APP] Server started

API Request Builders

const request = curry((baseURL, method, endpoint, data) => {
console.log(`${method} ${baseURL}${endpoint}`, data || "");
return fetch(`${baseURL}${endpoint}`, {
method,
headers: { "Content-Type": "application/json" },
body: data ? JSON.stringify(data) : undefined
});
});

// Configure for a specific API
const api = request("https://api.example.com");

// Create method-specific functions
const apiGet = api("GET");
const apiPost = api("POST");
const apiPut = api("PUT");
const apiDelete = api("DELETE");

// Use them
apiGet("/users");
apiPost("/users", { name: "Alice", email: "alice@example.com" });
apiPut("/users/1", { name: "Alice Updated" });
apiDelete("/users/1", null);

Event Handler Factories

const handleEvent = curry((action, elementId, event) => {
console.log(`Action: ${action}, Element: ${elementId}, Type: ${event.type}`);
});

const handleClick = handleEvent("click");
const handleSubmit = handleEvent("submit");

// Each returns a function that accepts the event object
document.getElementById("btn-save").addEventListener("click", handleClick("btn-save"));
document.getElementById("btn-delete").addEventListener("click", handleClick("btn-delete"));
document.getElementById("myForm").addEventListener("submit", handleSubmit("myForm"));

Validation Chains

const validate = curry((ruleName, ruleConfig, value) => {
switch (ruleName) {
case "minLength":
return value.length >= ruleConfig
? { valid: true }
: { valid: false, error: `Must be at least ${ruleConfig} characters` };
case "maxLength":
return value.length <= ruleConfig
? { valid: true }
: { valid: false, error: `Must be at most ${ruleConfig} characters` };
case "matches":
return ruleConfig.test(value)
? { valid: true }
: { valid: false, error: `Does not match required pattern` };
default:
return { valid: true };
}
});

// Create reusable validators
const minLength = validate("minLength");
const maxLength = validate("maxLength");
const matches = validate("matches");

const min3 = minLength(3);
const max50 = maxLength(50);
const isEmail = matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);

console.log(min3("hi")); // { valid: false, error: "Must be at least 3 characters" }
console.log(min3("hello")); // { valid: true }
console.log(isEmail("test@a.com")); // { valid: true }
console.log(isEmail("not-email")); // { valid: false, error: "Does not match required pattern" }

String Formatting

const formatWith = curry((template, ...values) => {
return values.reduce((str, val, i) => str.replace(`{${i}}`, val), template);
});

const greetTemplate = formatWith("Hello, {0}! Welcome to {1}.");
console.log(greetTemplate("Alice", "JavaScript"));
// "Hello, Alice! Welcome to JavaScript."

const errorTemplate = formatWith("Error {0}: {1} at line {2}");
console.log(errorTemplate("404", "Not Found", "42"));
// "Error 404: Not Found at line 42"

Lodash _.curry

The Lodash library provides a battle-tested _.curry function that handles edge cases you might not think of. It is the most widely used curry implementation in the JavaScript ecosystem.

Basic Usage

import _ from "lodash";

function add(a, b, c) {
return a + b + c;
}

const curriedAdd = _.curry(add);

// All calling styles work:
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
curriedAdd(1, 2, 3); // 6

Placeholder Arguments with _.curry.placeholder

Lodash supports placeholders, allowing you to skip arguments and fill them in later. This enables currying arguments in any order, not just left to right:

import _ from "lodash";

function formatDate(year, month, day) {
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}

const curriedFormat = _.curry(formatDate);

// Use placeholder (_) to skip the year
const formatThisYear = curriedFormat(2024);
console.log(formatThisYear(1, 15)); // "2024-01-15"
console.log(formatThisYear(12, 25)); // "2024-12-25"

// Skip the first argument, fix month to December
const decemberOf = curriedFormat(_, 12);
console.log(decemberOf(2024)(25)); // "2024-12-25"
console.log(decemberOf(2023)(31)); // "2023-12-31"

_.curryRight

Lodash also provides _.curryRight, which applies arguments from right to left:

import _ from "lodash";

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

const curriedGreet = _.curryRight(greet);

const greetAlice = curriedGreet("Alice");
console.log(greetAlice("Hello")); // "Hello, Alice!"
console.log(greetAlice("Hi")); // "Hi, Alice!"

Custom Arity

You can specify the arity (number of expected arguments) explicitly, which is useful for functions with default or rest parameters:

import _ from "lodash";

function sum(a, b, c = 0) {
return a + b + c;
}

console.log(sum.length); // 2 (default params not counted)

// Force curry to expect 3 arguments
const curriedSum = _.curry(sum, 3);
console.log(curriedSum(1)(2)(3)); // 6

Implementing a Lodash-Like Placeholder System

If you do not want to import Lodash, here is a simplified implementation with placeholder support:

const _ = Symbol("placeholder");

function curryWithPlaceholder(fn) {
const arity = fn.length;

return function curried(...args) {
// Check if we have enough non-placeholder args
const realArgs = args.filter(a => a !== _);

if (realArgs.length >= arity) {
return fn(...args.filter(a => a !== _));
}

return function(...nextArgs) {
// Replace placeholders with next arguments
const merged = [];
let nextIndex = 0;

for (const arg of args) {
if (arg === _ && nextIndex < nextArgs.length) {
merged.push(nextArgs[nextIndex++]);
} else {
merged.push(arg);
}
}

// Append any remaining nextArgs
while (nextIndex < nextArgs.length) {
merged.push(nextArgs[nextIndex++]);
}

return curried(...merged);
};
};
}

function volume(l, w, h) {
return l * w * h;
}

const curriedVolume = curryWithPlaceholder(volume);

const withHeight10 = curriedVolume(_, _, 10);
console.log(withHeight10(2, 3)); // 60 (2 * 3 * 10)
console.log(withHeight10(5, 5)); // 250 (5 * 5 * 10)

Common Mistakes and Pitfalls

Currying Functions with Variable Arguments

Currying relies on knowing how many arguments a function expects. Functions that accept a variable number of arguments do not work with automatic currying:

// This will NOT work as expected
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}

const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3));
// Problem: sum.length is 0 (rest params), so curry calls sum immediately

The fix is to specify the arity explicitly:

function curryN(n, fn) {
return function curried(...args) {
if (args.length >= n) {
return fn(...args);
}
return (...next) => curried(...args, ...next);
};
}

const add3 = curryN(3, (...nums) => nums.reduce((a, b) => a + b, 0));
console.log(add3(1)(2)(3)); // 6

Losing Readability with Excessive Currying

Currying improves code when used for creating reusable, specialized functions. But currying everything can make code harder to read:

// Too much currying: hard to understand
const result = curry((a, b, c, d, e) => a + b + c + d + e)(1)(2)(3)(4)(5);

// Just call the function normally
const result = add(1, 2, 3, 4, 5);
tip

Curry functions when you genuinely need to create specialized versions. If you are always passing all arguments at once, currying adds complexity without benefit.

Debugging Curried Functions

Curried functions create deep call chains that can be harder to debug. Named functions help:

// Anonymous: hard to debug
const add = curry((a, b) => a + b);

// Named: shows up in stack traces
const add = curry(function add(a, b) {
return a + b;
});

Quick Reference

// Manual currying
const add = a => b => c => a + b + c;

// Generic curry helper
function curry(fn) {
return function curried(...args) {
return args.length >= fn.length
? fn(...args)
: (...next) => curried(...args, ...next);
};
}

// Usage
const curriedFn = curry((a, b, c) => a + b + c);
curriedFn(1)(2)(3); // 6
curriedFn(1, 2)(3); // 6
curriedFn(1)(2, 3); // 6

// Partial application with bind
const addTen = add.bind(null, 10);

// Lodash
import _ from "lodash";
const curried = _.curry(fn);
const rightCurried = _.curryRight(fn);

Currying is not about making code shorter. It is about making functions more composable. When you curry a function, you give yourself the ability to build specialized tools from general ones, create clean data pipelines, and write code that reads like a description of what it does rather than how it does it.