Skip to main content

Functions and Closures Problems

These problems go deeper into functions: closures, higher-order functions, this, and callback patterns. If closures still feel fuzzy, revisit the Closure Function chapter first — every problem here builds on it.

1. Counter Factory

Write createCounter(start) that returns a counter object with increment, decrement, and value methods. The count itself must not be directly accessible from outside.

Example
const counter = createCounter(10);
counter.increment();
counter.increment();
counter.decrement();
counter.value(); // 11
counter.count; // undefined
Solution
const createCounter = (start = 0) => {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
};

count lives in the scope of createCounter and only the three returned functions close over it — that closure is the only way in. This is the classic "private variable" pattern: from 10, two increments and one decrement leave the count at 11, and counter.count is undefined because the object never exposes it.

2. Run It Only Once

Payment handlers must never charge twice, even if a button is double-clicked. Write once(fn) that lets fn run a single time; every later call returns the first result without running fn again.

Example
const initPayment = once((amount) => {
console.log(`Charging Rs ${amount}`);
return `TXN-${amount}`;
});

initPayment(2500); // logs "Charging Rs 2500", returns "TXN-2500"
initPayment(9999); // logs nothing, still returns "TXN-2500"
Solution
const once = (fn) => {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
};

Two variables live in the closure: a flag and the cached result. The first call flips the flag and stores the result; every later call short-circuits straight to the cached value. Note the second call's 9999 is ignored entirely — the original transaction is what gets returned.

3. Memoize

Write memoize(fn) that caches results, so calling the function again with the same arguments skips the computation.

Example
const slowSquare = memoize((n) => {
console.log(`computing ${n}...`);
return n * n;
});

slowSquare(9); // logs "computing 9...", returns 81
slowSquare(9); // returns 81 instantly, no log
slowSquare(4); // logs "computing 4...", returns 16
Solution
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn(...args));
}
return cache.get(key);
};
};

The Map lives in the closure and survives between calls. Serializing the arguments with JSON.stringify makes the cache work for multiple arguments, not just one. Memoization trades memory for speed — a cache hit is O(1), which is a huge win when fn is expensive and called repeatedly with the same inputs.

4. Debounce

Ayesha types in a search box, and firing an API request on every keystroke would hammer the server. Write debounce(fn, delay) that runs fn only after the calls stop for delay milliseconds.

Example
const search = debounce((query) => {
console.log(`Searching for: ${query}`);
}, 300);

search("A");
search("Ay");
search("Ayesha");
// after 300ms of silence, logs once: "Searching for: Ayesha"
Solution
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
};

timerId is remembered in the closure across calls. Every new call cancels the pending timer and schedules a fresh one, so only the final call in a rapid burst survives long enough to fire. The related pattern, throttle, is the opposite trade-off: it guarantees the function runs at most once per interval while events keep firing.

5. The Lost this

Rizwan wrote a user object with a greet method, but passing the method to setTimeout breaks it. Explain why the broken version fails and fix it.

Example
const user = {
name: "Rizwan",
greet() {
console.log(`Hello, ${this.name}`);
},
};

user.greet(); // "Hello, Rizwan"
setTimeout(user.greet, 100); // "Hello, undefined" — broken!
Solution
setTimeout(() => user.greet(), 100); // "Hello, Rizwan"

setTimeout(user.greet.bind(user), 100); // "Hello, Rizwan"

this is decided at call time, not where the function was written. user.greet() works because the call happens through user. But setTimeout(user.greet, ...) passes only the bare function — when the timer fires it, there is no user. in front, so this no longer points to the object and this.name is undefined. The arrow-function wrapper keeps the user.greet() call intact, while bind(user) creates a copy of the function with this permanently fixed. Both fixes are common in real code; pick whichever reads better.

6. Build a Pipe

Write pipe(...fns) that composes functions left to right: the output of one becomes the input of the next. Use it to compute a final product price: apply 17% tax, add Rs 200 shipping, then round.

Example
const addTax = (amount) => amount * 1.17;
const addShipping = (amount) => amount + 200;

const finalPrice = pipe(addTax, addShipping, Math.round);
finalPrice(3500); // 4295
Solution
const pipe =
(...fns) =>
(input) =>
fns.reduce((value, fn) => fn(value), input);

pipe is a higher-order function twice over: it takes functions as arguments and returns a new function. reduce threads the value through the pipeline — 3500 becomes 4095 after tax, 4295 after shipping, and Math.round cleans up the floating-point noise from multiplying by 1.17. Each step stays a tiny, testable function, and the pipeline reads like a sentence.