A frontend coding example is rarely a binary tree. It is a polyfill (
Array.prototype.map,Promise.all), an async utility (debounce, retry, a concurrency-limited pool), a UI component (accordion, autocomplete, infinite scroll) or a pure data-shaping function (flatten, group, LRU cache), and it is graded on the cases a hidden test file asserts: whether yourmappreserves sparse holes, whether[].reduce(fn)throws a TypeError, whether your accordion header is a button inside a heading witharia-expandedandaria-controls. This article works through each category with the assertion that breaks a naive answer.
What Counts as a Coding Example in a Frontend Loop
Search for coding interview examples and you mostly get algorithm problems in general-purpose languages. igotanoffer publishes a page of 47 coding interview examples with answers in common languages. AlgoExpert is a catalogue of data-structures-and-algorithms problems. CoderPad advertises examples for more than 42 languages and frameworks, organised as a directory of per-technology category pages rather than a set of worked problems. All three are useful for the round they target. None of them is the round you get when the job title says frontend.
The closest thing to a frontend list is interviewguide.dev's Practical Coding page, which names the right problems: fetching and displaying API data, implementing JavaScript array methods, building a loading bar, slider or image carousel, implementing Promise.all. It stops there. There are no implementations on the page, and it points readers to an external platform to actually practise. So you can learn that Promise.all will come up and still not learn what a correct one has to do.
That gap is what this article fills. Everything below is a problem you would be asked in a JavaScript or TypeScript coding round, with the specific behaviour a grader checks. Binary trees, dynamic programming and graph traversal are deliberately out of scope. Plenty of frontend loops do include an algorithm round, and if yours does, the general corpora cover it better than another list would.
The Four Kinds of Frontend Coding Example
Everything in this article belongs to exactly one of four categories, and each category is graded on something different. Knowing which one you are in tells you what to say out loud before you start typing.
Polyfills reimplement something the platform already provides. The grading criterion is spec fidelity at the edges, because the happy path takes two minutes and the test file is about the rest. Examples covered here: Array.prototype.map, Array.prototype.reduce, Array.prototype.flat, Function.prototype.bind, Promise.all, and a deep clone in the shape of structuredClone.
Async utilities wrap functions you do not control in order to manage time or concurrency. The grading criterion is timing, ordering and cancellation. Examples covered here: debounce, throttle, retry with backoff, and a concurrency-limited promise pool.
UI components are interactive widgets in the DOM or a framework. The grading criterion is the state model, the keyboard and ARIA contract, and teardown. Examples covered here: accordion, autocomplete, analog clock, infinite scroll.
Data shaping covers pure functions over data with no DOM and no clock. The grading criterion is boundary correctness and complexity. Examples covered here: flattening a nested object into dot paths, grouping and normalising an API payload, building a tree from a flat parent-id list, and an LRU cache.
Autocomplete needs the debounce from the async section; the example stays there.
Polyfill Examples: Rebuilding the Standard Library
The prompt is one sentence: implement Array.prototype.map without calling map. Write the loop and you are done in ninety seconds. Then the tests run.
const sparse = [1, , 3];
const mapped = sparse.map(x => x * 2);
console.log(mapped.length, 1 in mapped, mapped[2]);
const naive = [];
for (let i = 0; i < sparse.length; i++) naive.push(sparse[i] * 2);
console.log(naive.length, 1 in naive, naive[1]);
3 false 6
3 true NaN
map does not invoke its callback for empty slots, and it preserves the holes in the result. A push loop calls the callback on a hole, produces NaN, and turns a sparse array into a dense one. The fix is to test i in arr before calling, and to assign by index into a pre-sized array rather than pushing. The same round usually asks you to read the array off this rather than a parameter, and to support the thisArg second argument.
reduce has two separate traps in one method.
console.log([1, 2, , 4].reduce((a, b) => a + b));
console.log([1, 2, undefined, 4].reduce((a, b) => a + b));
console.log([].reduce((a, b) => a + b, 0));
try { [].reduce((a, b) => a + b); } catch (e) { console.log(e.name); }
7
NaN
0
TypeError
The callback runs only for indexes with assigned values, so a hole is skipped and an explicit undefined is not. An empty array with no initial value throws a TypeError. A polyfill that seeds the accumulator with arr[0] and starts at index 1 returns undefined for the empty case instead of throwing, which is a one-line assertion in any test file.
flat is the shortest of these and still has two behaviours candidates miss.
console.log(JSON.stringify([1, [2, [3, [4]]]].flat()));
console.log(JSON.stringify([1, [2, [3, [4]]]].flat(Infinity)));
console.log(JSON.stringify([1, 2, , 4, 5].flat()));
[1,2,[3,[4]]]
[1,2,3,4]
[1,2,4,5]
Depth defaults to 1, so a recursive implementation that always flattens fully is wrong by default. Flattening a sparse array removes the empty slots, which is the opposite of what map does with them.
Function.prototype.bind is where candidates write four correct lines and miss the constructor case.
function Point(x, y) {
this.x = x;
this.y = y;
}
const BoundPoint = Point.bind({ x: 'ignored' }, 1);
const p = new BoundPoint(2);
console.log(p.x, p.y);
console.log(p instanceof Point);
console.log(BoundPoint.name, BoundPoint.length);
1 2
true
bound Point 1
Called with new, a bound function behaves as though the target had been constructed: the prepended arguments still apply, the bound this is ignored, and the instance is an instanceof the target. A bound function's name is the target's name with a bound prefix, and its length is the target's length minus the number of bound arguments, floored at zero. Those two properties are cheap assertions, so they show up often.
Promise.all is the most-named example on every frontend-specific list and the least-specified one.
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 20));
const fast = Promise.resolve('fast');
Promise.all([slow, fast]).then(values => console.log(values.join(',')));
Promise.all([]).then(values => console.log('empty:', values.length));
console.log('synchronous line');
synchronous line
empty: 0
slow,fast
Three contract points. The fulfillment value is an array in the order of the promises passed, regardless of completion order, so results must be written to a fixed index rather than pushed. An empty iterable produces an already-fulfilled promise, and Promise.all resolves synchronously only in that case, so a counter that waits for count === length must handle zero before the loop. Any rejection rejects the outer promise immediately with that reason. If you finish early, the follow-up is usually Promise.any, which returns the first fulfilled value rather than the first settled one, and rejects with an AggregateError when every input rejects, including on an empty iterable.
Deep clone is the last common polyfill, and the interviewer's reference point is structuredClone.
const node = { name: 'MDN' };
node.itself = node;
const copy = structuredClone(node);
console.log(copy.itself === copy, copy === node);
class Vec { constructor(x) { this.x = x; } }
const clone = structuredClone(new Vec(3));
console.log(clone.x, clone instanceof Vec, clone.constructor.name);
true false
3 false Object
Circular references survive, which means your implementation needs a map of already-visited objects, and the recursive one-liner overflows the stack without it. The prototype chain is not walked or duplicated, so a class instance comes back as a plain object. Property descriptors, getters and setters are not duplicated, class private elements are not duplicated, and a RegExp's lastIndex is not preserved. Functions and DOM nodes cause a DataCloneError. Say which of these your version supports before you write it, because "deep clone" without a stated contract is an underspecified prompt and the interviewer is waiting to see whether you notice. There is more on this family of problems in the JavaScript coding interview guide.
Async Utility Examples: Debounce, Throttle, Retry, Promise Pool
These are the questions where the naive version looks right and the behaviour is wrong, because nothing prints when a timer misfires.
function debounce(fn, wait) {
let timer = null;
const debounced = (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { timer = null; fn(...args); }, wait);
};
debounced.cancel = () => { clearTimeout(timer); timer = null; };
return debounced;
}
const save = debounce(value => console.log('saved', value), 10);
save('a'); save('b'); save('c');
const cancelled = debounce(() => console.log('never runs'), 10);
cancelled('x'); cancelled.cancel();
setTimeout(() => console.log('done'), 40);
saved c
done
That is the trailing-edge version, which is the default reading of the word. The follow-ups are predictable: add a leading-edge option that fires on the first call and suppresses the rest of the window, add flush to invoke the pending call immediately, and decide what debounced() returns when nothing has been invoked yet. Throttle is the sibling question and the one with the most ambiguity: at most one call per window, but ask whether the interviewer wants the leading call, the trailing call, or both, because those are three different implementations and only one will pass their tests.
function throttle(fn, wait) {
let last = -Infinity;
return (...args) => {
const now = Date.now();
if (now - last < wait) return;
last = now;
fn(...args);
};
}
const send = throttle(value => console.log('sent', value), 20);
send('a');
send('b');
setTimeout(() => send('c'), 30);
sent a
sent c
b never arrives. The leading-edge version fires the first call in a window and drops the rest with nothing scheduled for the end, where the trailing version would log b when the window closed and the both-edges version would log both.
Retry adds a decision that has nothing to do with timers.
async function retry(fn, attempts = 3, baseDelay = 10) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await fn(i);
} catch (err) {
lastError = err;
if (i < attempts - 1) await new Promise(r => setTimeout(r, baseDelay * 2 ** i));
}
}
throw lastError;
}
let calls = 0;
retry(async () => {
calls++;
if (calls < 3) throw new Error('flaky');
return 'ok';
}).then(value => console.log(value, calls));
ok 3
Note the i < attempts - 1 guard: without it you sleep for the backoff after the final failure, which nobody wants and a timing test will catch. The other thing to raise out loud is which errors are retryable. Retrying a 400 forever is a bug, and saying so is worth more than the loop itself.
A concurrency-limited pool is the natural escalation from Promise.all, and it inherits the same ordering guarantee.
let active = 0, peak = 0;
const task = (ms, value) => async () => {
peak = Math.max(peak, ++active);
await new Promise(r => setTimeout(r, ms));
active--;
return value;
};
async function pool(tasks, limit) {
const out = new Array(tasks.length);
let next = 0;
const worker = async () => {
while (next < tasks.length) { const i = next++; out[i] = await tasks[i](); }
};
await Promise.all(Array.from({ length: limit }, worker));
return out;
}
pool([task(30, 'a'), task(10, 'b'), task(1, 'c')], 2).then(r => console.log(r.join(','), peak));
a,b,c 2
The shared cursor is what keeps concurrency at the limit rather than processing in fixed batches: a worker that finishes early picks up the next task instead of waiting for its batch. Results still land at their input index. The Redux-Saga effect runner is the same muscle applied to a different shape of problem.
UI Component Examples: Accordion, Autocomplete, Analog Clock, Infinite Scroll
An accordion looks like the easiest question on any list and it is the one where the accessibility contract is most often missed. The ARIA Authoring Practices pattern asks for the header title to be a button wrapped in a heading element with an appropriate aria-level, aria-expanded on the button reflecting whether the panel is visible, and aria-controls on the button referencing the panel's id. Enter or Space expands a collapsed panel, and collapses an expanded one where the implementation supports collapsing. A pair of divs with an onClick handler fails all of that while looking identical on screen.
The state model is the other half, and it is where the requirements question lives.
function toggle(state, id) {
const open = new Set(state.open);
if (open.has(id)) {
if (state.collapsible || open.size > 1) open.delete(id);
} else {
if (!state.multiple) open.clear();
open.add(id);
}
return { ...state, open };
}
let s = { open: new Set(['a']), multiple: false, collapsible: false };
s = toggle(s, 'b');
console.log([...s.open].join('|'));
s = toggle(s, 'b');
console.log([...s.open].join('|'));
b
b
Two flags, four behaviours: one panel at a time or many, and whether the last open panel can be closed. Ask which before you write the reducer, because the version above refuses to close the only open panel and a test written against the other assumption fails immediately. A single-panel version of the same contract is the FAQ disclosure problem.
Autocomplete is the combobox pattern, and the pattern is specific. The input carries role="combobox" with aria-expanded, and aria-controls pointing at a popup with role="listbox" (grid, tree and dialog are also permitted); aria-autocomplete is none, list or both depending on the behaviour; and aria-activedescendant tracks the active option while DOM focus stays on the input. Down Arrow opens the popup, Escape closes it. Move real focus into the list and the input stops receiving keystrokes, which is the classic broken implementation. On top of the ARIA work sit three async concerns: debounce the query (from the async category above), drop stale responses by comparing a request id or an abort signal so a slow first request cannot overwrite a fast second one, and render distinct loading, empty-result and error states. The stale response is the one candidates skip, and it is four lines.
let latest = 0;
function search(query, ms) {
const id = ++latest;
return new Promise(resolve => setTimeout(() => resolve(query), ms)).then(value => {
if (id !== latest) console.log('discarded', value);
else console.log('rendered', value);
});
}
search('sl', 20);
search('slow', 5);
rendered slow
discarded sl
A response renders only if it is still the newest request, so the slow first fetch is dropped instead of overwriting the fast second. An AbortController per keystroke does the same job and cancels the request as well. Basic autocomplete is the place to get the skeleton fluent before adding those.
The analog clock is a rendering-math question wearing a timer.
function handAngles(date) {
const seconds = date.getSeconds() + date.getMilliseconds() / 1000;
const minutes = date.getMinutes() + seconds / 60;
const hours = (date.getHours() % 12) + minutes / 60;
return [hours * 30, minutes * 6, seconds * 6];
}
const at330 = new Date(2026, 0, 1, 3, 30, 0);
console.log(handAngles(at330).join(' '));
console.log((at330.getHours() % 12) * 30);
105 180 0
90
At half past three the hour hand sits at 105 degrees, halfway between the 3 and the 4. The naive version pins it to 90 and the clock looks wrong to anyone who glances at it. Keep the angle function pure, as above, and it is testable by passing a fixed Date — no rendering and no fake timers. Save jest.setSystemTime for the component that calls new Date() on each tick.
Infinite scroll is graded on the observer lifecycle. With no root specified, an IntersectionObserver uses the document's viewport; rootMargin defaults to 0px 0px 0px 0px, and threshold defaults to 0, meaning a one-pixel intersection change fires the callback. Positive rootMargin is how you prefetch before the sentinel is visible. The failures are consistent: firing a second fetch while the first is in flight, forgetting to stop observing once the list is exhausted, and never calling unobserve or disconnect on teardown.
useEffect(() => {
let loading = false;
const observer = new IntersectionObserver(async ([entry]) => {
if (!entry.isIntersecting || loading) return;
loading = true;
const rows = await loadPage(pageRef.current++);
loading = false;
if (rows.length === 0) observer.disconnect();
});
observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, []);
The in-flight flag lives in the closure rather than in state, so a second intersection during the fetch cannot start a duplicate and nothing re-renders to reset it. One disconnect covers both exhaustion and unmount.
Data Shaping Examples: Flatten, Group, Normalise, LRU
These are pure functions with no DOM and no clock, which makes them the fastest to test and the easiest to get subtly wrong.
function flatten(obj, prefix = '', out = {}) {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) flatten(value, path, out);
else out[path] = value;
}
return out;
}
console.log(JSON.stringify(flatten({ a: { b: { c: 1 } }, d: [1, 2], e: {} })));
{"a.b.c":1,"d":[1,2]}
The empty object vanishes, because there is no leaf to emit. Whether that is correct depends on a requirement nobody states: is { e: {} } supposed to round-trip? Arrays raise the same question. This version treats them as leaves, and the alternative emits d.0 and d.1. Ask.
Grouping and normalising an API payload is the same skill in the shape you meet at work: turn a list into { byId, allIds } in one pass, and decide what happens when two rows share an id. Last write wins and collect-into-an-array are both defensible; picking one silently is not. Building a tree from a flat parent-id list is the escalation: one pass to build the id-to-node map, a second to attach each node to its parent, which is O(n) and beats the O(n²) filter-inside-a-map version. Its boundary inputs are rows whose parent id matches nothing, children that appear before their parents, duplicate ids, and a cycle that turns a naive recursive render into an infinite loop.
An LRU cache is the one data-shaping example with a strict complexity expectation: O(1) for both operations.
class LRU {
constructor(limit) { this.limit = limit; this.map = new Map(); }
get(key) {
if (!this.map.has(key)) return undefined;
const value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
}
put(key, value) {
if (this.map.has(key)) this.map.delete(key);
else if (this.map.size >= this.limit) this.map.delete(this.map.keys().next().value);
this.map.set(key, value);
}
}
const cache = new LRU(2);
cache.put('a', 1);
cache.put('b', 2);
cache.get('a');
cache.put('c', 3);
console.log([...cache.map.keys()].join(','));
a,c
Map iterates in insertion order, so delete-then-set moves a key to the most-recent end and the first key from keys() is the least recently used. b is evicted even though it was inserted after a, because the get('a') refreshed a. The follow-ups start with a limit of zero, which the version above gets wrong: keys().next().value is undefined on an empty map, delete(undefined) is a no-op, and the entry is set anyway, so new LRU(0) holds one item until put opens with if (this.limit <= 0) return;. After that come a put that updates an existing key without changing the size, and whether get on a missing key should return undefined or throw.
One trap that shows up whenever a shaping question ends in sorting: with no compare function, sort orders by the UTF-16 code unit sequence of the stringified elements, so [1, 30, 4, 21, 100000].sort() gives [1, 100000, 21, 30, 4]. Since ES2019 the sort is specified as stable, which is what makes sorting by a secondary key first a valid technique.
How Interviewers Actually Score These Examples
A function that passes every test can still fail the round, and a half-finished one can pass it. The reason is that the sequence is what the interviewer can see: a passing function tells them the code works, the order tells them how you got there.
Four moves, in this order. Restate the contract in your own words, including the part the prompt left out: does this accordion allow multiple open panels, does this deep clone need to handle Map and Set, is this throttle leading or trailing. Enumerate your test cases out loud before writing the implementation, and make the list specific: the empty array, the single element, the duplicate key, the rejection. Ship a naive version that works on the happy path, and say that it is naive as you write it. Then name the edge cases and the cleanup, and fix what time allows.
That order is why a partly finished answer passes. An interviewer who has heard you say "an empty iterable has to resolve immediately, I will handle that after the main loop" has already seen the thing they were testing for, even if the clock runs out first. A candidate who silently produces a complete implementation with a push-based Promise.all has demonstrated the opposite, whatever the tests say.
Frontend rounds run in a range of environments — the interviewer's shared editor, a browser workspace with a preview pane, sometimes a repository you clone — so whether a test file exists, and whether you can see it, changes what a good answer looks like. Three questions to ask in the first minute, because the answers change how you spend it: may I use libraries, are tests expected from me, and is there a hidden suite my code runs against. There is a full worked example of that rhythm in this 35-minute walkthrough.
The last thing being scored is how you take a change. Most rounds end with a modified requirement: now the accordion must allow multiple panels, now the pool must abort on the first failure. Whether your code absorbs that in two lines or needs a rewrite is a direct read on the abstraction you chose, and that is what the change is there to expose.
How to Rehearse These Examples Before Your Loop
Reading a finished solution rehearses recognition. The round tests production, and those are different skills, so structure practice around producing.
A session that works: pick one example from each of the four categories. Before writing any implementation, write the test list in plain English, five to eight assertions including the empty input, the single-element input and the error condition. Timebox the implementation, and when the box ends, stop and compare what you wrote against the list you wrote first. The gap between them is your actual weakness, and it is usually the same one every time. Then, and only then, look at a reference solution.
Do the second pass in the framework your interviewer uses. The accordion state model above is framework-agnostic, but its cleanup, its reactivity and its keyboard wiring are not: the React version turns on effect dependencies and refs, the Vue version on watchers and template refs, the Angular version on signals and change detection, the vanilla TypeScript version on event delegation and manual attribute updates. Guides for the specific rounds are worth reading alongside the practice: React interview questions and Angular interview questions both break down what each framework round actually asks.
This is the part a static list cannot give you, which is why UIReady exists as a workspace rather than an answer key. It holds 660+ interview questions in a Sandpack editor (Sandpack is CodeSandbox's toolkit for live-running code editing experiences, powered by the bundler used on CodeSandbox), running your code against real Jest tests, with solution walkthroughs in React, Vue, Angular or vanilla TypeScript. Start with Test Runner if you want to internalise how an assertion suite thinks before you start failing them. If you would rather own the library outright than rent it for one loop, the lifetime plan is on the upgrade page.