
Overview
Recently, I was working through a JavaScript coding challenge: implement a curry function.
At first glance, it seemed straightforward. I’ve worked with JavaScript professionally, understand recursion, use closures regularly, and have seen curried functions before. How hard could it be?
Then I spent far longer than I expected staring at my screen wondering where my arguments had gone.
The Challenge
The goal was to transform a function like this:
function add(a, b) {
return a + b;
}into something that could be called like this:
const curriedAdd = curry(add);
curriedAdd(1)(2); // 3Simple enough, or so I thought.
My First Mistake Was Misunderstanding
arguments
My initial instinct was to inspect the arguments object inside the curry function.
function curry(fn) {
console.log(arguments);
}When I ran:
const curriedAdd = curry(add);I saw:
[Arguments] { '0': [Function: add] }At first, this confused me.
I kept thinking:
When I call
curriedAdd(1), shouldn’t that1show up incurry’s arguments?
The answer is no.
That realization was my first breakthrough.
When you write:
curry(add)(1)you’re actually performing two separate function calls.
First:
curry(add)Then, after that returns:
returnedFunction(1)The 1 never reaches curry. It belongs to the function returned by curry.
Once I understood that, things started making more sense.
My Second Mistake Was Forgetting to Return a Function
At one point I had:
function curry(fn) {
console.log(arguments);
}Then I tried:
const curriedAdd = curry(add);
curriedAdd(1);Which produced:
TypeError: curriedAdd is not a functionThe reason was obvious in hindsight.
My curry function wasn’t returning anything, so JavaScript returned undefined.
That meant:
const curriedAdd = undefined;And trying to call undefined(1) naturally exploded.
Not exactly my finest debugging moment.
My Third Mistake Was Losing Arguments
After I started returning functions, I hit another wall.
I was repeatedly collecting new arguments but accidentally throwing away previous ones.
Conceptually, I was doing this:
Call 1: [1]
Call 2: [2]instead of:
Call 1: [1]
Call 2: [1, 2]I kept treating each invocation as a replacement instead of an accumulation.
That turned out to be the central idea behind currying.
Each function call isn’t replacing previous arguments.
It’s extending the collection.
The Missing Piece
fn.length
One thing I hadn’t considered was:
How does the curried function know when to stop collecting arguments and finally execute?
JavaScript already provides the answer:
function add(a, b) {}
console.log(add.length); // 2The length property tells us how many parameters were declared.
That means we can compare:
args.lengthagainst:
fn.lengthIf we’ve collected enough arguments, execute the original function.
Otherwise, keep collecting.
That was the moment the entire implementation clicked.
The Solution
Eventually, I arrived at:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return function (...nextArgs) {
return curried(...args, ...nextArgs);
};
};
}The implementation isn’t particularly long.
What’s interesting is what it’s doing.
The returned function remembers previous arguments through closures.
When I call:
curriedAdd(1)it doesn’t return a number.
It returns another function that remembers the value 1.
Later, when I call:
curriedAdd(1)(2)the second invocation has access to both the new argument and the previously captured argument.
That’s the part I intellectually understood before.
Actually implementing it forced me to see it in action.
What I Learned
The biggest lesson wasn’t about currying.
It was about closures.
I’ve used closures countless times in React hooks, event handlers, callbacks, and state management.
But implementing curry forced me to confront what closures actually do:
They preserve data after the surrounding function has finished executing.
The function isn’t magically waiting around.
The data survives because the returned function closes over the variables it needs.
That’s a concept I’ve known for years.
Yet somehow implementing a 15-line utility function made it click in a way documentation never did.
Sometimes the most valuable coding exercises aren’t the ones that teach you a new concept.
They’re the ones that expose gaps in concepts you thought you already understood.
Currying wasn’t difficult because it required advanced JavaScript.
It was difficult because it forced me to connect recursion, closures, function metadata, argument spreading, and higher-order functions into one mental model, and judging by the amount of time I spent staring at a function returning another function returning another function, I’d say it did its job.