
Building JavaScript Utilities From Scratch
A mental model for frontend utility problems like debounce, throttle, and Promise.all
Building JavaScript Utilities From Scratch
I used to look at frontend utility problems like debounce, throttle, memoize, custom Promise.all, or even re-implementing Array.prototype.map and think people were just memorizing patterns.
Especially coming from Java.
Because Java feels structurally honest.
Methods belong to classes. Threads are visible. State is explicit. Memory feels predictable.
JavaScript initially felt like things were happening magically behind the curtains.
Functions surviving after execution. Variables existing after stack frames disappear. Timers not actually blocking execution. Objects inheriting from other objects dynamically at runtime.
None of that felt natural initially.
Then eventually I realized almost every frontend utility problem is built from the same few runtime concepts repeated in different combinations:
- higher order functions
- closures
- prototypes
- the event loop
- async scheduling
- state persistence
Once those clicked properly, utility problems stopped feeling random.
This article is basically the mental model I wish I had earlier.
Not the polished documentation version.
The version that actually made things click.
The First Thing That Felt Illegal In JavaScript
Functions returning functions.
Coming from Java, this already feels slightly weird.
In Java, methods belong to classes. You usually don't casually pass methods around without wrapping them in interfaces or lambdas.
In JavaScript, functions are just values.
function createMultiplier(multiplier){
return function(num){
return num * multiplier;
};
}
const double = createMultiplier(2);
console.log(double(5));The multiplication is not the important part here.
This line is:
return function(num)A function returning another function.
That pattern alone powers:
- debounce
- throttle
- curry
- memoize
- wrappers
- middleware
- hooks
- half the frontend ecosystem honestly
At first I treated this like syntax trivia.
Then I realized higher-order functions are basically how JavaScript builds reusable runtime behavior.
The outer function acts like a configuration layer.
The returned inner function becomes the actual executable logic.
That distinction matters a lot later.
Closures Are Just Persistent State Hiding Inside Functions
This was probably the biggest mental shift coming from Java.
Because in Java, stack frames disappearing means local variables disappear too.
That mental model breaks immediately in JavaScript.
function createVault(){
let secret = "1234";
return function(){
return secret;
};
}
const openVault = createVault();
console.log(openVault());The weird part here is not the returned function.
The weird part is:
secret still exists.
Even after createVault() already finished execution.
That feels fundamentally wrong initially if you're used to stack-frame based reasoning.
People usually explain closures using phrases like:
"functions remember their lexical environment"
which is technically correct but honestly terrible for intuition.
What actually helped me:
A closure is basically a function carrying hidden persistent state around with it.
That's it.
The returned inner function keeps references to variables from its parent scope alive.
And suddenly utility problems start making sense.
For example:
function once(fn){
let hasRun = false;
let result;
return function(...args){
if(!hasRun){
result = fn(...args);
hasRun = true;
}
return result;
};
}Without closures this utility is impossible.
You need persistent hidden state:
hasRunresult
without touching globals.
This is basically private instance state without classes.
And honestly once closures click:
- React hooks make more sense
- debounce becomes understandable
- throttle becomes understandable
- memoization becomes trivial
- async wrappers stop looking magical
A huge chunk of frontend JavaScript is honestly just:
functions carrying state around invisibly
debounce Finally Made Sense Once I Stopped Thinking About Delays
For a while I misunderstood debounce completely.
I thought:
"debounce delays execution"
That's not really the important part.
The important part is: debounce continuously destroys previously scheduled execution.
That changes the mental model entirely.
function debounce(func, wait){
let timeoutId = null;
return function(...args){
if(timeoutId !== null){
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func(...args);
timeoutId = null;
}, wait);
};
}The closure is the important part again.
timeoutId survives across calls.
Without that persistence you can't cancel old timers.
Imagine a search box:
- user types "J"
- timer starts
- user types "JA"
- previous timer gets killed
- user types "JAV"
- timer gets killed again
- user types "JAVA"
- timer gets killed again
- user finally stops typing
- now the function executes
The utility is basically controlled cancellation.
Not delayed execution.
That distinction mattered a lot once I started implementing it manually.
throttle Feels Similar Until You Realize It's The Opposite Philosophy
I used to confuse debounce and throttle constantly.
Because both use timers. Both use closures. Both wrap functions.
But their behavior philosophy is opposite.
Debounce says:
"wait until things calm down"
Throttle says:
"I don't care how chaotic things get, execution only happens once per interval"
function throttle(func, wait){
let isThrottled = false;
return function(...args){
if(isThrottled) return;
func(...args);
isThrottled = true;
setTimeout(() => {
isThrottled = false;
}, wait);
};
}This is basically a gate lock.
Once execution happens:
- lock the gate
- ignore everything else
- unlock later
That's the whole utility.
Once I started seeing these utilities as state machines instead of syntax problems, they became dramatically easier.
The Event Loop Is Why JavaScript Feels Like It's Lying Sometimes
This one caused genuine confusion initially.
Especially this classic interview question:
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");My initial instinct was:
1
2
3
4Wrong.
Actual output:
1
4
3
2This is where JavaScript's runtime model starts mattering more than syntax.
Promises go into the microtask queue.
Timers go into the macrotask queue.
And microtasks always execute first once synchronous execution finishes.
The important realization here:
setTimeout(fn, 0) does not mean:
execute immediately
It means:
schedule this for later after higher-priority work finishes
That single distinction explains a huge amount of weird async behavior in JavaScript.
Rebuilding Native Methods Changed How I Viewed Arrays
Implementing map, filter, and reduce manually was probably the point where prototypes finally clicked properly.
Especially this:
Array.prototype.myMap = function(callback){
const transformed = [];
for(let i = 0; i < this.length; i++){
if(i in this){
transformed.push(
callback(this[i], i, this)
);
}
}
return transformed;
};The important part here is not even the loop.
It's this:
thisInside prototype methods:
this becomes the array instance that invoked the method.
numbers.myMap(...)means:
this === numbers
That dynamic binding behavior is everywhere in JavaScript.
And it becomes dangerous fast if you don't actually understand it.
reduce Is Basically State Tracking Disguised As A Functional Utility
This one looked complicated until I realized:
reduce is just iterative state accumulation.
That's it.
Array.prototype.myReduce = function(callback, initialValue){
const hasInitialValue = arguments.length > 1;
let accumulator = hasInitialValue
? initialValue
: this[0];
let startIndex = hasInitialValue
? 0
: 1;
for(let i = startIndex; i < this.length; i++){
accumulator = callback(
accumulator,
this[i],
i,
this
);
}
return accumulator;
};If you've ever written:
int sum = 0;
for(...) {
sum += arr[i];
}you already understand reduce.
The only difference is: the state update logic becomes abstracted into a callback.
That's it.
Memoization Is Basically Dynamic Programming Wearing JavaScript Clothes
This one became easy instantly because of DSA background.
function memoize(func){
const cache = {};
return function(...args){
const key = JSON.stringify(args);
if(key in cache){
return cache[key];
}
const result = func(...args);
cache[key] = result;
return result;
};
}This is literally:
- hashmap lookup
- avoid recomputation
- return cached value
It's dynamic programming logic.
Just wrapped inside closures.
Promise.all Was The First Async Utility That Actually Felt Architectural
This one stopped feeling like syntax and started feeling like orchestration.
function myPromiseAll(promises){
return new Promise((resolve, reject) => {
const results = [];
let completedCount = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((value) => {
results[index] = value;
completedCount++;
if(completedCount === promises.length){
resolve(results);
}
})
.catch(reject);
});
});
}The tricky part is not waiting for promises.
The tricky part is: maintaining result ordering despite asynchronous completion timing.
That's why index tracking matters.
Otherwise faster promises would reorder your output accidentally.
The Biggest Realization
Eventually I realized most frontend utility problems are the same few runtime concepts repeated differently.
debounce?
Closure + timers.
throttle?
Closure + execution locking.
memoize?
Closure + hashmap.
once?
Closure + boolean state.
Promise.all?
Async orchestration + counters.
curry?
Recursion + argument accumulation.
That's why memorizing solutions blindly feels painful.
Because the actual skill is recognizing the underlying runtime pattern underneath the syntax.
Once the runtime model becomes clear, the utilities stop feeling random.
