Functions That Remember: Rethinking Currying in JavaScript
Currying transforms a function into a sequence of functions that each take one argument, allowing context to be captured progressively and enabling more reusable, composable logic.
At first glance, currying feels like an unnecessary complication.
You take a function that accepts multiple argumentsā¦
and turn it into a chain of functions that take one argument at a time.
What used to look simple:
add(2, 3)Becomes something that feels almost excessive:
add(2)(3)So the reaction is natural:
Why make it harder than it needs to be?
But that question assumes something subtleā
that functions are only meant to run, not to evolve.
The Moment a Function Stops Being Static
In most code, a function is treated like a one-time action.
You give it everything it needs.
It gives you a result.
Done.
But not all information is available at the same time.
Sometimes:
- part of the context is known early
- the rest comes later
And thatās where currying quietly changes the model.
Instead of executing immediately, the function begins to wait.
It accepts one piece of informationā¦
and returns a new function that remembers it.
A Function That Learns in Stages
Consider this shift:
const greet = (greeting) => (name) => `${greeting}, ${name}`Youāre no longer just calling a function.
Youāre shaping it.
const sayHello = greet("Hello")At this point, the function already knows something.
It doesnāt need to be told again.
When you later call:
sayHello("Rishi")It completes the picture.
Not because everything was passed at onceā
but because the function carried part of the context forward.
This Isnāt About Syntax
Itās tempting to think currying is just about splitting arguments.
But that misses whatās actually happening.
The real shift is this:
From:
A function that needs everything now
To:
A function that can be built over time
Each step:
- captures a piece of context
- returns a more specific function
Until eventually, nothing is missing.
Where It Starts to Feel Useful
This becomes more meaningful when repetition appears.
Take something like an API call.
Without much thought, you might write:
fetchData(baseUrl, endpoint, token)Every call repeats:
- the same base URL
- the same token
With currying, that repetition disappearsānot by removing it, but by relocating it.
const createFetcher = (baseUrl) => (token) => (endpoint) => { ... }Now the function can be shaped step by step:
const api = createFetcher("https://api.com")
const authorizedApi = api(token)At this point, the function already carries its environment.
Calling:
authorizedApi("users")feels less like assembling inputsā¦
and more like using a tool thatās already been prepared.
But Thereās a Cost
This is where the tension appears.
Currying improves structureā
but it doesnāt always improve readability.
A single line like:
createFetcher("api.com")(token)("users")can feel harder to follow than its non-curried version.
Not because itās wrongā
but because it introduces layers.
And layers demand attention.
Two Kinds of Readability
This is where things become clearer.
There isnāt just one kind of readability.
Thereās:
Local readability
How easy a line is to understand in isolation
And:
Structural readability
How easy a system is to extend, reuse, and evolve
Currying often trades the first for the second.
It makes individual lines slightly harder to parseā
but makes the overall system more flexible.
When It Starts to Break Down
Currying becomes a problem when itās used without purpose.
When thereās no real separation of concerns.
When everything is already known at once.
When functions are split simply because they can be.
Or when too many layers are introduced without meaning:
fn(a)(b)(c)(d)At that point, the structure no longer clarifies.
It obscures.
The Role of Naming
Thereās one detail that quietly determines whether currying feels elegant or confusing.
Naming.
Compare:
createFetcher("api.com")(token)("users")With:
const api = createFetcher("api.com")
const authorizedApi = api(token)
authorizedApi("users")The logic hasnāt changed.
But the second version gives each step a role.
It reduces the mental effort required to follow the flow.
Currying doesnāt stand on its ownā
it depends on how you present it.
A More Balanced Approach
In practice, many developers donāt fully curry everything.
They stop halfway.
const createFetcher = (baseUrl, token) => (endpoint) => { ... }This keeps:
- some structure
- less nesting
A compromise between flexibility and clarity.
And often, that balance is what makes code feel natural.
A Different Way to Think About It
Currying isnāt about breaking functions apart.
Itās about deferring decisions.
Instead of asking for everything upfrontā¦
it allows information to arrive when itās available.
A function doesnāt just execute.
It accumulates context.
A Final Thought
At the surface, currying looks like extra layers.
But underneath, itās about something more subtle.
Not how functions runā
but how they adapt.
Because sometimes, the most useful function isnāt the one that does everything immediately.
Itās the one that can waitā¦
rememberā¦
and become exactly what you need, step by step.