FrontendPro
Senior frontend.
Mastered.
150 must-solve interview questions, production-ready answers, and the follow-ups that decide the offer.
15 answers free. No account needed to browse.
mapLimit() with AbortSignal
Run async tasks with at most N in flight. Keep results in order, fail fast, and stop when the caller aborts.
- Results keep input order
- Never more than limit tasks
- Reject on the first failure
- Respect the AbortSignal
- keeps input order
- never runs more than limit tasks
- rejects as soon as one task fails
- stops when the signal aborts
- handles an empty list
Gettingtherightoutputistheeasypart.Thefollow-upquestionsarewheresenioroffersarewon.
The FrontendPro editor
A real editor.
Right in your browser.
Write JavaScript or React, run it instantly, and check it against an interviewer's test suite.
1import { useState } from "react";23export default function App() {4 const [count, setCount] = useState(0);5 return (6 <button onClick={() => setCount(count + 1)}>7 Clicked {count} times8 </button>9 );10}
Runs instantly.
Your code runs in your browser the moment you press Run. Nothing to install, nothing to wait for.
Tests like an interviewer.
One click checks every edge case an interviewer would try, with a clear reason for each failure.
Live React preview.
Write a component and watch it render beside your code. Click it, break it, fix it.
Practise the way interviews actually go.
Three steps, on every one of the 150 questions.
Step 1
Read the problem. Start the clock.
Every question is a real interview task with a time budget for its level, from 15 minutes on easy to an hour on hard.
Implement mapLimit() with at most N tasks running at once.
- Results must keep input order.
- Reject as soon as one task fails.
- Never run more than limit tasks.
Step 2
Solve it. Then run the tests.
Write your solution in the editor and check it against the edge cases an interviewer would try, right in your browser.
async function mapLimit(items, limit, fn) {
const results = [];
let next = 0;
const worker = async () => {
while (next < items.length) {
const i = next++;
results[i] = await fn(items[i]);
}
};
await Promise.all(Array.from({ length: limit }, worker));
return results;
}All 3 tests passed
- keeps input order
- never runs more than limit tasks
- rejects on the first failure
Step 3
Face the follow-ups.
Answer the questions an interviewer asks next out loud, then reveal the model answer. This is where senior offers are decided.
Interviewer
What happens when one task rejects?
Promise.all rejects at once, but workers already running keep going. Track a failed flag so no new task starts.
- Can a caller cancel halfway?
- What about 100,000 items?
Answer out loud first. Then reveal.
Implement mapLimit() with at most N tasks running at once.
- Results must keep input order.
- Reject as soon as one task fails.
- Never run more than limit tasks.
Everything you need.
Nothing you don't.
Follow-ups with model answers.
Try each one out loud, then reveal how a senior engineer would answer it.
- What happens when one task rejects?Reveal
- Can a caller cancel halfway through?
- How do you avoid starving low priority work?
150
must-solve questions.
25 easy, 75 medium, 50 hard.
Export anything.
PDF, Word, Markdown or Anki. Offline, on any device.
Track every solve.
Mark questions solved, bookmark the ones to revise, and pick up where you left off.
Free to start.
One full answer in every track, 15 in all. Every problem statement is public, and anyone can run code in the editor.
Start with the free answersBuilt for the keyboard.
Next, previous and search, without the mouse.
15 tracks. From polyfills to system design.
- Easy, 10 questionsJavaScript fundamentalsRebuild the core array and function methods every interviewer expects you to know cold.
- Easy, 10 questionsStrings, objects & algorithmsHash maps, stacks and object utilities that show up in every warm-up round.
- Easy, 5 questionsBrowser & DOM fundamentalsTraverse, query and wire up the DOM without a framework.
- Medium, 10 questionsJavaScript utilities & FPdebounce, throttle, curry, deep clone, LRU — the utility belt of senior rounds.
- Medium, 10 questionsPromises & asyncPromise combinators and async helpers implemented from scratch.
- Medium, 10 questionsConcurrency & schedulingPools, queues, rate limiters and batching with bounded concurrency.
- Medium, 10 questionsReact hooks & stateCustom hooks without stale closures, plus a store built from first principles.
- Medium, 10 questionsReact application patternsSearch, optimistic UI, pagination, forms and virtualization done right.
- Medium, 10 questionsUI machine codingAccessible, keyboard-friendly widgets built under interview time pressure.
- Medium, 15 questionsBrowser, network & performanceFetch wrappers, routers, workers, caching and Web Vitals in practice.
- Hard, 10 questionsAdvanced async concurrencySchedulers, cancellation, backpressure and resilience patterns.
- Hard, 10 questionsAdvanced React & renderingReconciliation, hooks internals, concurrent rendering and SSR edge cases.
- Hard, 10 questionsAdvanced JS, browser & architectureSandboxes, parsers, reactivity, offline sync and secure messaging.
- Hard, 10 questionsFrontend system designDesign the frontends of WhatsApp, Docs, YouTube, Sheets and more.
- Hard, 10 questionsAdvanced machine coding & DSASpreadsheets, editors, IDEs and the data structures behind them.
Free sample answer
#26 Implement debounce() with cancel() and flush()
Problem
Implement debounce(fn, wait). It returns a debounced function that delays calling fn until wait milliseconds have passed since the last call. Every new call restarts the timer, so a burst of calls produces a single call to fn with the latest arguments.
The debounced function also has two methods:
cancel(): drop the pending call, if there is one.flush(): run the pending call now and return its result. If nothing is pending, return the result of the last call.
const save = debounce((text) => api.save(text), 500);
input.addEventListener("input", (e) => save(e.target.value));
window.addEventListener("beforeunload", () => save.flush()); // do not lose the last edit
cancelButton.onclick = () => save.cancel();Requirements
- Call
fnwith the arguments andthisof the latest call. - The debounced function returns the result of the last completed call (
undefinedbefore the first). - After
fnhas run, the debounced function can be used again.
Starter code
function debounce(fn, wait = 0) {
// Your code here
}Step-by-step
Keep the state in a closure
You need four variables that outlive each call: the timer id, the latest arguments, the latest this, and the last result.
Restart the timer on every call
Each call saves its arguments and this, clears the previous timer and sets a new one. Only the last timer in a burst survives, so fn runs once, wait ms after the final call.
Use a regular function for the wrapper
this must come from how the debounced function is called, for example as obj.save(). An arrow function would freeze this to wherever debounce was called.
Put the real call in one place
invoke() clears the timer id, calls fn.apply(savedThis, savedArgs), clears the saved arguments and stores the result. Both the timer and flush use it, so they always behave the same.
Implement cancel and flush
cancel clears the timer and forgets the saved arguments. flush does nothing but return the last result when no call is pending; otherwise it clears the timer and calls invoke() right away.
Approach
A closure holds one timer and the latest call's arguments. Every call replaces both; when the timer finally fires, fn runs once with the latest arguments. cancel and flush are the two ways to settle the pending call early.
Solution
function debounce(fn, wait = 0) {
let timer = null;
let savedArgs;
let savedThis;
let result;
function invoke() {
const args = savedArgs;
const self = savedThis;
timer = null;
savedArgs = savedThis = undefined;
result = fn.apply(self, args);
return result;
}
function debounced(...args) {
savedArgs = args;
savedThis = this;
clearTimeout(timer);
timer = setTimeout(invoke, wait);
return result;
}
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
savedArgs = savedThis = undefined;
};
debounced.flush = function () {
if (timer === null) return result;
clearTimeout(timer);
return invoke();
};
return debounced;
}Complexity
Time: O(1) per call. Space: O(1): one timer and the latest arguments.
Edge cases
- A burst of calls produces exactly one call to
fn, with the last arguments. cancel()with nothing pending does nothing.flush()with nothing pending returns the last result without callingfn.- After
flush(), the old timer cannot fire a second call. wait = 0still defers the call to a later task.
Tests
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
test("calls fn once, after the wait, with the latest arguments", async () => {
const calls = [];
const debounced = debounce((x) => calls.push(x), 40);
debounced(1);
debounced(2);
debounced(3);
expect(calls).toEqual([]);
await wait(80);
expect(calls).toEqual([3]);
});
test("restarts the timer on every call", async () => {
let calls = 0;
const debounced = debounce(() => calls++, 50);
debounced();
await wait(30);
debounced();
await wait(30);
expect(calls).toBe(0);
await wait(50);
expect(calls).toBe(1);
});
test("keeps the this value of the latest call", async () => {
let seen = null;
const obj = {
name: "editor",
save: debounce(function () { seen = this.name; }, 10),
};
obj.save();
await wait(40);
expect(seen).toBe("editor");
});
test("cancel drops the pending call", async () => {
let calls = 0;
const debounced = debounce(() => calls++, 20);
debounced();
debounced.cancel();
await wait(50);
expect(calls).toBe(0);
});
test("flush runs the pending call immediately and returns its result", async () => {
let calls = 0;
const debounced = debounce((x) => { calls++; return x * 2; }, 50);
debounced(4);
expect(debounced.flush()).toBe(8);
expect(calls).toBe(1);
await wait(80);
expect(calls).toBe(1);
});
test("flush with nothing pending returns the last result", async () => {
let calls = 0;
const debounced = debounce(() => ++calls, 10);
expect(debounced.flush()).toBeUndefined();
debounced();
await wait(30);
expect(debounced.flush()).toBe(1);
expect(calls).toBe(1);
});
test("can be used again after it fired", async () => {
const calls = [];
const debounced = debounce((x) => calls.push(x), 10);
debounced("a");
await wait(30);
debounced("b");
await wait(30);
expect(calls).toEqual(["a", "b"]);
});Written by Sumit Singh.
Author of the open-source Frontend Interview Questions handbook, with 3,000+ free questions on GitHub. Premium is the deep end of that work: fewer questions, fully worked.
See the free handbookPay once. Keep it forever.
No subscription. Less than the cost of one mock interview.
Monthly
For one interview cycle.
₹99
for 30 days. One payment, it does not renew.
- All 150 solutions with edge cases and complexity
- Follow-up questions with model answers
- Progress tracking and bookmarks
- Exports to PDF, Word, Markdown and Anki
Lifetime
Best valueEverything, forever. Every new answer included.
₹199
once. The price of two months, for good.
- All 150 solutions with edge cases and complexity
- Follow-up questions with model answers
- Progress tracking and bookmarks
- Exports to PDF, Word, Markdown and Anki
Launch price. It goes up once all 150 answers are published.
- Pay with UPI, card or net banking through Razorpay
- Access unlocks the moment payment succeeds
- Full refund within 7 days
- Receipts for your company's learning budget
Compare plans
Questions? Answers.
Is the monthly plan a subscription?
No. ₹99 buys 30 days of full access, paid once. Nothing renews automatically; buy again whenever you want more time.
What does Lifetime include?
Every current and future answer in this question set, follow-ups, progress tracking and exports, for a single ₹199 payment.
Will the price go up?
Yes. ₹199 is the launch price while answers are still being published. If you buy Lifetime now, you keep access forever at no extra cost.
How do I pay?
Through Razorpay, with UPI, cards, net banking or wallets.
Can I try it first?
Yes. Every question and problem statement is public, and one full answer in each of the 15 tracks is free.
Can I get a refund?
Yes, within 7 days of purchase. See the refund policy for details.