JavaScript Coding Interview Questions
Short answer
A JavaScript coding round asks you to implement small utilities live, debounce, throttle, curry, memoize, deep clone, flatten and a Promise.all polyfill. Each one below has a working commented solution, the follow-up the interviewer asks next, and the specific bug that costs candidates the mark.
| Problem | The idea in one line | The follow-up you will get |
|---|---|---|
| debounce | Reset a timer on every call; run once the calls stop | Add a leading edge and a cancel method |
| throttle | Remember the last run; ignore calls inside the window | Make sure the trailing call still fires |
| curry | Collect arguments until you have enough, then apply | Handle default parameters, where fn.length lies |
| memoize | Cache by a key derived from the arguments | Object arguments, and when to evict |
| flatten | Recurse into arrays while reducing | Do it iteratively, without the call stack |
| deep clone | Rebuild every level instead of copying references | Dates, Maps and circular references |
| Promise.all | Count completions, store results by index | Now write allSettled, race and any |
How a JavaScript coding round actually works
The coding round is usually 30 to 45 minutes in a shared editor with no autocomplete and no internet. You get one or two small utilities to implement, almost always something that already exists in Lodash, and the interviewer watches you type. The prompts are deliberately familiar, because a problem you have seen before removes the excuse of not knowing the algorithm and leaves only the thing they want to observe: how you reason, how you name things, and whether you notice the cases nobody mentioned.
The rhythm that scores well is the same every time. Restate the problem in your own words, ask one or two clarifying questions, say what your approach will be and what it costs, write the straightforward version, trace one concrete input out loud, then improve it. Candidates who start typing within ten seconds of hearing the prompt almost always end up rewriting, and the rewrite is what runs them out of time.
Expect to be asked to extend your own solution at the end. That is not a sign the first part went badly, it is precisely how the interviewer separates a memorised snippet from real understanding, which is why every section below ends with the follow-up you should be ready for.
Implement debounce
Debounce delays a function until a quiet period has passed. Every call clears the pending timer and starts a new one, so a burst of twenty keystrokes produces exactly one execution, delay milliseconds after the last of them. Search-as-you-type, window resize handlers and autosave all use it.
1function debounce(fn, delay = 300) {
2 let timer;
3
4 function debounced(...args) {
5 clearTimeout(timer);
6 timer = setTimeout(() => fn.apply(this, args), delay);
7 }
8
9 debounced.cancel = () => clearTimeout(timer);
10 return debounced;
11}
12
13const search = debounce((q) => console.log("search:", q), 200);
14search("r"); search("re"); search("rea"); search("react");
15// → search: react once, 200ms after the last call
16// → nothing else the first three timers were clearedThree details separate a full-mark answer from a half-mark one. Using `fn.apply(this, args)` preserves the receiver, so the debounced function still works when it is an object method, writing `debounced` itself as an arrow function silently breaks that, because it captures `this` from where it was defined. Calling `clearTimeout` on an undefined timer is harmless, so no guard is needed and adding one suggests you are unsure. And attaching a `cancel` method is the detail interviewers wait for: a debounced call still pending when a React component unmounts will fire against a component that no longer exists unless something cancels it.
Add a leading edge - fire immediately on the first call, then debounce the rest.
Track whether a timer is currently pending. If there is none, invoke `fn` straight away; either way, set a fresh timer whose only job is to clear the pending flag once the quiet period ends. The common bug is clearing that flag inside the same timer that calls `fn`, which makes the leading and trailing edges fight each other and produces a double invocation on every burst.
Implement throttle
Throttle guarantees a maximum rate rather than a quiet period: at most one execution every interval milliseconds, no matter how often it is called. Scroll tracking, mousemove handlers and rate-limited API calls want throttle, not debounce, with debounce, a user who never stops scrolling never triggers the handler at all.
1function throttle(fn, interval = 200) {
2 let last = 0;
3 let timer = null;
4
5 return function (...args) {
6 const remaining = interval - (Date.now() - last);
7
8 if (remaining <= 0) {
9 last = Date.now();
10 fn.apply(this, args); // leading edge
11 } else if (timer === null) {
12 timer = setTimeout(() => { // trailing edge
13 last = Date.now();
14 timer = null;
15 fn.apply(this, args);
16 }, remaining);
17 }
18 };
19}
20
21const onScroll = throttle((y) => console.log("y:", y), 100);
22// fired every 16ms for one second:
23// → ~10 logs, about 100ms apart, and the FINAL scroll position is includedThe naive version - `if (Date.now() - last > interval) fn()`, is what most candidates write, and it fails on the case that matters. The last call in a burst lands inside the cooling window and is simply dropped, so a scroll handler never sees where the user actually stopped and a progress bar freezes short of the bottom. Parking that pending call in a timer for the remaining time gives you the trailing edge and fixes it. Noticing the dropped trailing call out loud is worth more than the code itself.
When would you use debounce instead of throttle?
Debounce when only the settled state matters, the finished search query, the final window size, the last edit before autosave. Throttle when you need regular updates during a continuous action, scroll progress, drag position, live analytics. Search-as-you-type is debounce; infinite scroll is throttle. If you genuinely cannot decide, ask whether the handler needs to run during the action or only after it, and the answer falls out.
Implement a curry function
Currying converts `f(a, b, c)` into a function you can call as `f(a)(b)(c)`, or `f(a, b)(c)`, or any other split of the arguments. The implementation is four lines and rests entirely on `fn.length`, the declared arity of a function, which is the piece candidates most often cannot recall under pressure.
1function curry(fn) {
2 return function curried(...args) {
3 return args.length >= fn.length
4 ? fn.apply(this, args)
5 : (...rest) => curried.apply(this, [...args, ...rest]);
6 };
7}
8
9const volume = curry((l, w, h) => l * w * h);
10
11console.log(volume(2)(3)(4)); // → 24
12console.log(volume(2, 3)(4)); // → 24
13console.log(volume(2)(3, 4)); // → 24
14console.log(volume(2, 3, 4)); // → 24
15
16const unitDepth = volume(1); // partial application, reusable
17console.log(unitDepth(5, 6)); // → 30`fn.length` counts only the parameters before the first default value or rest parameter, so this `curry` quietly misbehaves on `(a, b = 1) => ...`, whose length is 1, it will call the function after a single argument. If the interviewer hands you exactly that, the fix is to accept the intended arity explicitly as `curry(fn, arity)`. Volunteering the limitation before you are asked about it is one of the cheapest ways to look senior in this round, and it takes one sentence.
Implement memoize
Memoize caches results so repeated calls with the same arguments skip the work. The single-argument version is trivial; the interesting part is the key function, because `JSON.stringify(args)`, the default nearly everyone reaches for, breaks on objects whose keys are in a different order, on functions, and on anything containing `undefined`. Taking the key function as a parameter is how you answer that before it is asked.
1function memoize(fn, keyFn = (...args) => JSON.stringify(args)) {
2 const cache = new Map();
3
4 return function (...args) {
5 const key = keyFn(...args);
6 if (cache.has(key)) return cache.get(key); // has(), not a truthy check
7 const result = fn.apply(this, args);
8 cache.set(key, result);
9 return result;
10 };
11}
12
13let calls = 0;
14const square = memoize((n) => { calls++; return n * n; });
15
16console.log(square(4), square(4), calls);
17// → 16 16 1 the second call never ran the function
18
19const fib = memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
20console.log(fib(35));
21// → 9227465 instant; un-memoized this is ~30 million callsThis is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299Flatten a deeply nested array
Flatten turns `[1, [2, [3, [4]]]]` into `[1, 2, 3, 4]`. The built-in `arr.flat(Infinity)` does it in one call and you should say so first, knowing the built-in is not cheating, it is what you would actually ship, and then write the manual version, because that is what is being asked.
1const nested = [1, [2, [3, [4, [5]]]]];
2
3console.log(nested.flat(Infinity));
4// → [ 1, 2, 3, 4, 5 ] what you would actually ship
5
6// Recursive, honouring a depth argument like the real method
7const flatten = (arr, depth = Infinity) =>
8 depth < 1
9 ? arr.slice()
10 : arr.reduce(
11 (out, x) => out.concat(Array.isArray(x) ? flatten(x, depth - 1) : x),
12 []
13 );
14
15console.log(flatten(nested)); // → [ 1, 2, 3, 4, 5 ]
16console.log(flatten(nested, 1)); // → [ 1, 2, [ 3, [ 4, [ 5 ] ] ] ]
17
18// Iterative - no call stack, survives 100k levels of nesting
19function flattenIterative(arr) {
20 const stack = [...arr];
21 const out = [];
22 while (stack.length) {
23 const x = stack.pop();
24 if (Array.isArray(x)) stack.push(...x);
25 else out.push(x);
26 }
27 return out.reverse(); // popped from the end, so reverse once
28}
29
30console.log(flattenIterative(nested));
31// → [ 1, 2, 3, 4, 5 ]Supporting a depth argument and offering the iterative version are what turn a one-liner into a conversation. Recursion exhausts the call stack somewhere around ten thousand levels of nesting, which never happens with real data but is exactly the constraint an interviewer adds when your first answer arrives too quickly. Having the stack-based version ready means the follow-up costs you two minutes instead of derailing the round.
Deep clone an object
A deep clone copies every level, so mutating the copy cannot reach the original. `structuredClone` is built into every modern browser and Node 17 and above, handles Dates, Maps, Sets, typed arrays and circular references, and is the correct production answer. It cannot clone functions, DOM nodes or class prototypes, say that, then write the recursive version.
1const original = { a: 1, nested: { list: [1, 2] }, when: new Date("2026-01-01") };
2
3const copy = structuredClone(original);
4copy.nested.list.push(3);
5console.log(original.nested.list); // → [ 1, 2 ] untouched
6console.log(copy.when instanceof Date); // → true
7
8function deepClone(value, seen = new WeakMap()) {
9 if (value === null || typeof value !== "object") return value;
10 if (seen.has(value)) return seen.get(value); // circular reference
11 if (value instanceof Date) return new Date(value);
12 if (value instanceof Map)
13 return new Map([...value].map(([k, v]) => [k, deepClone(v, seen)]));
14 if (value instanceof Set)
15 return new Set([...value].map((v) => deepClone(v, seen)));
16
17 const out = Array.isArray(value)
18 ? []
19 : Object.create(Object.getPrototypeOf(value));
20 seen.set(value, out); // register BEFORE recursing
21 for (const key of Reflect.ownKeys(value)) out[key] = deepClone(value[key], seen);
22 return out;
23}
24
25const cyclic = { name: "root" };
26cyclic.self = cyclic;
27console.log(deepClone(cyclic).self.name);
28// → root no infinite recursionImplement Promise.all from scratch
This is the async question that turns up in almost every mid-level round, because it cannot be answered by pattern-matching. You have to know that results are stored by index rather than pushed, and that a separate counter, not the results array length, decides when everything is done.
1function promiseAll(items) {
2 return new Promise((resolve, reject) => {
3 const results = [];
4 let done = 0;
5
6 if (items.length === 0) return resolve([]); // the empty case, always forgotten
7
8 items.forEach((item, i) => {
9 Promise.resolve(item).then((value) => { // works for non-promise values too
10 results[i] = value; // by index, never push
11 if (++done === items.length) resolve(results);
12 }, reject); // first rejection wins
13 });
14 });
15}
16
17promiseAll([Promise.resolve(1), 2, new Promise((r) => setTimeout(() => r(3), 50))])
18 .then(console.log);
19// → [ 1, 2, 3 ] input order, even though 3 finished last
20
21promiseAll([Promise.resolve(1), Promise.reject(new Error("boom"))])
22 .catch((e) => console.log(e.message));
23// → boomPushing into `results` is the bug this question exists to catch: the array ends up in completion order, so a fast third promise appears first and the caller silently receives scrambled data. Using `results.length` as the completion counter has the same root cause, assigning `results[2]` before `results[0]` gives the array a length of 3 while two slots are still empty holes, and the promise resolves early with undefined in them. Wrapping each item in `Promise.resolve` rather than calling `item.then` directly is what lets plain values sit in the input array, which is real behaviour of the native method and a detail interviewers notice.
Now write allSettled.
Same skeleton, three changes: never call reject, wrap each outcome as `{ status: "fulfilled", value }` or `{ status: "rejected", reason }`, and increment the counter in both branches. `race` is shorter still - attach `resolve` and `reject` to every item and let whichever settles first win. `any` is `race` inverted: resolve on the first fulfilment, and reject with an `AggregateError` only once every input has rejected.
Implement an event emitter
Pub/sub in about fifteen lines. It comes up because every framework has one, and because the unsubscribe handle quietly tests whether you think about cleanup without being told to.
1class EventEmitter {
2 #listeners = new Map();
3
4 on(event, fn) {
5 if (!this.#listeners.has(event)) this.#listeners.set(event, new Set());
6 this.#listeners.get(event).add(fn);
7 return () => this.off(event, fn); // unsubscribe handle
8 }
9
10 once(event, fn) {
11 const off = this.on(event, (...args) => { off(); fn(...args); });
12 return off;
13 }
14
15 off(event, fn) {
16 this.#listeners.get(event)?.delete(fn);
17 }
18
19 emit(event, ...args) {
20 // copy first: a listener that unsubscribes during emit mutates the set
21 for (const fn of [...(this.#listeners.get(event) ?? [])]) fn(...args);
22 }
23}
24
25const bus = new EventEmitter();
26const off = bus.on("save", (id) => console.log("saved", id));
27bus.once("save", () => console.log("first save only"));
28
29bus.emit("save", 1);
30// → saved 1
31// → first save only
32bus.emit("save", 2);
33// → saved 2
34off();
35bus.emit("save", 3);
36// → nothingReturning the unsubscribe function from `on` is the detail worth volunteering, it is exactly what a React `useEffect` cleanup wants, and it saves the caller from holding a reference to the precise function they registered. Copying the listener set before iterating is the subtler one: `once` removes itself while `emit` is still running, and mutating a `Set` mid-iteration is how listeners get silently skipped. Both are small, and both are the kind of thing that only appears in code written by someone who has debugged a real event bus.
The array problems that still come up
Frontend rounds rarely go deep into algorithms, but three array problems appear often enough to be worth rehearsing, and all three are a single pass with a hash map or a reduce.
1// Two Sum - indices of the pair adding to target, one pass, O(n)
2function twoSum(nums, target) {
3 const seen = new Map();
4 for (let i = 0; i < nums.length; i++) {
5 if (seen.has(target - nums[i])) return [seen.get(target - nums[i]), i];
6 seen.set(nums[i], i);
7 }
8 return [];
9}
10console.log(twoSum([2, 7, 11, 15], 9)); // → [ 0, 1 ]
11
12// Group by - the Lodash function people reach for daily
13const groupBy = (arr, keyFn) =>
14 arr.reduce((out, item) => {
15 const key = keyFn(item);
16 (out[key] ??= []).push(item);
17 return out;
18 }, {});
19
20console.log(groupBy(["ant", "bee", "ape"], (w) => w[0]));
21// → { a: [ 'ant', 'ape' ], b: [ 'bee' ] }
22
23// Chunk - split into groups of n
24const chunk = (arr, n) =>
25 Array.from({ length: Math.ceil(arr.length / n) }, (_, i) =>
26 arr.slice(i * n, i * n + n)
27 );
28
29console.log(chunk([1, 2, 3, 4, 5], 2)); // → [ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]State the complexity as you go, `twoSum` is O(n) time and O(n) space, and the brute-force nested loop it replaces is O(n squared). Interviewers frequently have a box on the scorecard for whether complexity was mentioned unprompted, and filling it costs you one sentence at the moment you finish typing.
What actually loses the round
Almost nobody fails these questions because they could not produce the algorithm. The losses cluster in a small set of habits, all of which are fixable in an afternoon of deliberate practice.
- →Silence. Ten minutes of typing with no narration leaves the interviewer nothing to score except the final code.
- →Coding before restating the problem, then discovering halfway through that the requirement was different.
- →Ignoring `this` and the arguments, the debounce that works standalone and breaks the moment it is an object method.
- →No cleanup: timers, listeners and in-flight requests left running with no way to cancel them.
- →Skipping the empty input, the single element and the already-handled case, every single time.
- →Naming everything a, b and x under pressure, then losing track of them yourself five lines later.
- →Claiming the solution works without ever tracing one concrete input out loud.
Frequently asked questions
- What are the most common JavaScript coding interview questions?
- Debounce, throttle, curry, memoize, deep clone, flatten and a Promise.all polyfill cover the majority of frontend coding rounds. Below those, an event emitter, a retry-with-backoff wrapper and the array problems, Two Sum, group by, chunk, appear regularly. All of them are Lodash functions, which is not a coincidence: the interviewer wants a problem you cannot fail purely on unfamiliarity.
- How long is a JavaScript coding round?
- Usually 30 to 45 minutes for one or two problems, in a shared editor with no autocomplete and no internet access. Budget the first three minutes for clarifying the requirement and stating your approach; candidates who skip that step routinely spend ten minutes rewriting later.
- What is the difference between debounce and throttle?
- Debounce runs the function once after the calls stop; throttle runs it at most once per interval while the calls are still coming. Search-as-you-type wants debounce, scroll position tracking wants throttle. The clearest way to say it in an interview: if the user never pauses, a debounced handler never fires at all, and a throttled one keeps firing at a steady rate.
- How do I implement debounce in JavaScript?
- Hold a timer in a closure. On every call, clear the pending timer and start a new one that invokes fn.apply(this, args) after the delay. Return the wrapper with a cancel method attached so a pending call can be discarded when a component unmounts. The this-binding and the cancel handle are the two details that separate a full answer from a partial one.
- Why shouldn't I use JSON.parse(JSON.stringify(obj)) to deep clone?
- It drops functions and undefined, converts Dates to strings, turns NaN and Infinity into null, empties Maps and Sets, and throws on circular references. Use structuredClone in modern browsers and Node 17 and above, or a recursive clone with a WeakMap when you also need class instances preserved.
- How do I write my own Promise.all?
- Return a new Promise, keep a results array and a completion counter, and for each input call Promise.resolve(item).then(...) so plain values work too. Assign each result at its own index, resolve when the counter reaches the input length, and pass reject straight through so the first failure rejects the whole thing. Resolve immediately for an empty array.
- Do I need to know time and space complexity for a frontend interview?
- You need to be able to state it for whatever you just wrote, O(n) versus O(n squared), and what the extra memory buys you. You very rarely need to derive complexity for anything harder than that. Saying it unprompted is frequently worth a scored point on its own, which makes it the highest-return sentence in the round.
- Can I use built-in methods like flat() and structuredClone?
- Name them first, then implement by hand. Naming the built-in shows you know what you would actually ship, and the implementation is what is being tested. Answering only with the built-in reads as dodging the question, and the interviewer has no way to tell it apart from not knowing.
- What is currying used for in real code?
- Partial application - fixing the leading arguments to produce a specialised function, which is how selector factories, pre-configured loggers and middleware chains are built. In an interview it is really testing whether you know fn.length and can comfortably return a function that returns a function.
- How do I practise for a JavaScript coding round?
- Implement each utility from a blank file, timed, without looking anything up, then repeat the same set a week later. The goal is that the mechanics cost you no thought, so all your attention during the round goes to edge cases and explanation. Reading a solution is not the same as having typed it three times.
- What should I do if I get stuck during the round?
- Say what you are stuck on and what you have already ruled out, then write the brute-force version and improve it from there. A working slow solution with a stated plan to optimise scores far higher than a blank editor, and interviewers give hints freely to candidates who make their thinking visible.
- Is a JavaScript coding round the same as a machine coding round?
- No. A coding round asks for pure JavaScript utilities in a shared editor; a machine coding round asks you to build a working UI component, tabs, autocomplete, a modal, usually in React. Most product companies in India run both, in separate slots, and the preparation for each is different.
This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299