FrontendPro

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.

frontendpro / questions / maplimit
44:12
#103Hard~60 min

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
solution.js Run Run tests
TestsConsole
  • 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.

App.jsxReact
1import { useState } from "react";
2
3export default function App() {
4 const [count, setCount] = useState(0);
5 return (
6 <button onClick={() => setCount(count + 1)}>
7 Clicked {count} times
8 </button>
9 );
10}
Preview

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.

#46Medium 35:00

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.
Start solving35 minutes on the clock

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.

solution.jsRun tests
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.

0
must-solve questions
0
tracks
0
difficulty levels
0
export formats

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.

72%
Easy
18 of 25
55%
Medium
41 of 75
18%
Hard
9 of 50

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 answers
jk/

Built for the keyboard.

Next, previous and search, without the mouse.

15 tracks. From polyfills to system design.

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 fn with the arguments and this of the latest call.
  • The debounced function returns the result of the last completed call (undefined before the first).
  • After fn has 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 calling fn.
  • After flush(), the old timer cannot fire a second call.
  • wait = 0 still 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"]);
});
SS

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 handbook

Pay 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 value

Everything, 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

MonthlyLifetime
All 150 solutions with edge cases and complexity
Follow-up questions with model answers
Run the test suite in the editor
PDF, Word, Markdown and Anki exports
New answers as they are publishedWhile your pass lastsForever
Access30 daysFor life
Price₹99 once₹199 once

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.

Your next offer
starts here.