Sitemap

Closures + Currying: The Power Duo That Will Level Up Your JavaScript

6 min readFeb 24, 2026

--

Press enter or click to view image in full size
Photo by Flipsnack on Unsplash

If you’ve been writing JavaScript for a while, you’ve probably encountered closures and currying as separate concepts. But here’s the thing — the real magic happens when you combine them. Together, they unlock elegant, reusable, and composable code patterns that will fundamentally change how you think about functions.

The 30-Second Refresher

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

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

// After: one argument at a time
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
curriedAdd(1)(2)(3); // 6

Closures let an inner function remember variables from an outer function’s scope, even after the outer function has returned.

function outer(x) {
return function inner(y) {
return x + y; // 'x' is remembered via closure
};
}

const addFive = outer(5);
addFive(3); // 8 — still remembers x=5
addFive(10); // 15 — still remembers x=5

Now here’s the key insight:

Currying creates the structure (a chain of functions).
Closures provide the memory (preserving state across those calls).

Together, they give you flexible, composable functions that “remember” their history.

How the Partnership Works

Consider this curried function:

function multiply(a) {
return function(b) {
return function(c) {
return a * b * c;
};
};
}

When you call multiply(2), you get back a function that remembers a = 2 through a closure. Call that with (3), and you get another function that remembers both a = 2 and b = 3. The final call (4) has access to everything it needs: 2 * 3 * 4 = 24.

Each step in the curry chain creates a new closure that captures everything that came before.

multiply(2)    → function(b) { ... }     // closure: a=2
↓(3) → function(c) { ... } // closure: a=2, b=3
↓(4) → 24 // uses all via closure

This means you can stop the chain at any point and create reusable, partially configured functions:

const double = multiply(2);
const doubleThenTriple = double(3);

doubleThenTriple(4); // 24
doubleThenTriple(5); // 30
doubleThenTriple(10); // 60

Building a Generic curry Utility

So far we’ve been manually currying functions — wrapping them in nested return function calls by hand. That works, but it doesn't scale. What if you want to curry any existing function?

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

// We want this to work:
const curriedAdd = curry(add);
curriedAdd(3)(4); // 7

// And partial application too:
const alreadyAddedThree = curriedAdd(3);
alreadyAddedThree(4); // 7

This is where closures and currying truly come together — we need to write a curry function that transforms any multi-argument function into a curried one automatically.

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));
};
};
}

That’s it — just 9 lines. But there’s a lot going on. Let’s break it down.

Get Aravind S’s stories in your inbox

Join Medium for free to get updates from this writer.

The key mechanism is fn.length — every JavaScript function has a .length property that tells you how many parameters it expects. curry uses this to decide: do I have enough arguments yet, or should I keep collecting?

function add(a, b) { return a + b; }
console.log(add.length); // 2

const curriedAdd = curry(add);

When you call curriedAdd(3):

  1. args = [3], and fn.length = 2
  2. args.length (1) < fn.length (2) — not enough arguments yet
  3. Returns a new function that remembers [3] via closure
  4. This new function waits for more arguments

When you then call that with (4):

  1. nextArgs = [4], concatenated with the closed-over [3] → [3, 4]
  2. Calls curried again with [3, 4]
  3. args.length (2) >= fn.length (2) — we have enough!
  4. Calls the original fn(3, 4) → returns 7

The Closure Is Doing the Heavy Lifting. Notice what’s happening with memory here. Each intermediate call creates a closure that accumulates arguments:

const curriedAdd = curry(add);          // closure: fn=add
const addThree = curriedAdd(3); // closure: fn=add, args=[3]
const result = addThree(4); // closure: fn=add, args=[3,4] → executes!

The args array from each call is captured in the closure of the returned function, carried forward, and concatenated with new arguments on the next call. This is the exact same closure + currying partnership, just generalized.

It Works with Any Arity

The beauty of this approach is it handles functions of any argument count:

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

const curriedVolume = curry(volume);

// All of these work:
curriedVolume(2)(3)(4); // 24
curriedVolume(2, 3)(4); // 24 — pass multiple args at once
curriedVolume(2)(3, 4); // 24 — or at different steps
curriedVolume(2, 3, 4); // 24 — or all at once

// Partial application at any level:
const boxes2Wide = curriedVolume(2);
const boxes2x3 = boxes2Wide(3);
boxes2x3(4); // 24
boxes2x3(10); // 60

Here’s where the generic curry really pays off — you don't have to restructure existing functions:

function logEvent(level, module, message) {
console.log(`[${level}] [${module}] ${message}`);
}

const log = curry(logEvent);

// Create specialized loggers through partial application
const error = log('ERROR');
const dbError = error('Database');
const authError = error('Auth');

dbError('Connection timeout'); // [ERROR] [Database] Connection timeout
authError('Token expired'); // [ERROR] [Auth] Token expired

// The original function stays unchanged
logEvent('INFO', 'API', 'OK'); // Still works normally

You wrote logEvent as a normal function. curry made it composable without modifying the original code. That's the real power.

Five Patterns You’ll Actually Use

  1. Configuration Builder

Progressive configuration is one of the most practical applications. Instead of passing a massive options object, build it step by step:

function createConfig(appName) {
return function(environment) {
return function(port) {
return { app: appName, env: environment, port: port };
};
};
}

const myApp = createConfig('MyApp');
const myAppProd = myApp('production');

const server1 = myAppProd(8080); // { app: 'MyApp', env: 'production', port: 8080 }
const server2 = myAppProd(3000); // { app: 'MyApp', env: 'production', port: 3000 }

Both servers share the same base configuration. No duplication, no risk of typos.

2. API Request Builder

This is one of the most practical applications you’ll encounter in production codebases. Think about how many times you repeat the same base URL, headers, and endpoint across API calls. Currying lets you lock those in once and build specialized callers.

function api(baseUrl) {
return function(endpoint) {
return function(method) {
return function(body) {
return fetch(`${baseUrl}${endpoint}`, {
method,
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' }
});
};
};
};
}

const myAPI = api('https://api.example.com');
const usersEndpoint = myAPI('/users');
const createUser = usersEndpoint('POST');
const getUsers = usersEndpoint('GET');

// Each function remembers everything above it in the chain
createUser({ name: 'Alice', age: 30 });

3. Secure API with Hidden Credentials

Closures are perfect for data privacy. The API key below is captured in a closure and can never be accessed directly:

function createSecureAPI(apiKey) {
return function(endpoint) {
return function(data) {
return fetch(endpoint, {
headers: { 'Authorization': `Bearer ${apiKey}` },
body: JSON.stringify(data)
});
};
};
}

const api = createSecureAPI('secret-key-12345');
const postUser = api('/users');

postUser({ name: 'Alice' });
// apiKey is hidden — no way to extract it from `api` or `postUser`

4. Memoization

Closures naturally create a space for caches that persist across calls:

function memoizedAdd(a) {
const cache = {}; // Private cache, shared by all calls to the returned function

return function(b) {
if (cache[b] !== undefined) {
console.log(`Cache hit: ${a} + ${b}`);
return cache[b];
}
console.log(`Computing: ${a} + ${b}`);
cache[b] = a + b;
return cache[b];
};
}

const add5 = memoizedAdd(5);
add5(3); // "Computing: 5 + 3" → 8
add5(3); // "Cache hit: 5 + 3" → 8 (instant!)

5. Composable Pipelines

Curried functions slot perfectly into composition patterns:

const pipe = (...fns) => (value) =>
fns.reduce((acc, fn) => fn(acc), value);

const add = (n) => (x) => x + n;
const multiply = (n) => (x) => x * n;
const square = (x) => x * x;

const calculate = pipe(
add(5),
multiply(2),
square
);

calculate(3); // (3+5)=8 → (8*2)=16 → (16²)=256

When to Choose This Pattern

Currying + closures shine when you need progressive specialization — starting with a general function and narrowing it down through a series of configuration steps. They’re ideal for building reusable loggers, validators, API clients, event handler factories, and function pipelines.

They’re less ideal when all arguments are always available at once, or when the function signature is simple enough that partial application adds more ceremony than value.

The general heuristic: if you find yourself passing the same first few arguments over and over, that’s a currying opportunity. If you need private state that persists across calls, closures are your tool.

The Takeaway

Currying gives you the structure — a chain of single-argument functions with delayed execution and natural partial application. Closures give you the memory — persistent state, private variables, and independent chains.

Together, they’re one of the most powerful patterns in functional JavaScript. Once you start seeing opportunities for this combination, you’ll find them everywhere.

Happy Reading!

--

--