FrontendAtlas
Interview Questions
Home>JavaScript interview questions> JavaScript coding challenges >Take Latest (Abort Previous Requests)

Take Latest (Abort Previous Requests)

intermediate
By FrontendAtlas Editorial · Updated Jul 14, 2026
Implement takeLatest(fn) that wraps an async function and guarantees only the most recent call can resolve. Each new call aborts the previous one via AbortController, and stale results must not update state. This prevents out-of-order responses in search-as-you-type, autocomplete, and live filtering. Concepts: async, cancellation, race control.

Arguments

  • fn: (signal: AbortSignal, ...args: any[]) => Promise<T> — Async worker to run. It receives an AbortSignal as the first argument, followed by your call arguments.

Returns

(...args: any[]) => Promise<T> — A wrapped function that aborts the previous call and resolves only the latest result.
Examples
const searchLatest = takeLatest((signal, q) =>
  fetch(`/api/search?q=${q}`, { signal }).then(r => r.json())
);

searchLatest('rea');
searchLatest('react'); // aborts the previous call

const latest = takeLatest(async (signal, id) => {
  const res = await fetch(`/user/${id}`, { signal });
  return res.json();
});

const user = await latest(42);

Solution

Overview

Keep at most one AbortController for the currently in-flight call. When a new call starts, abort that previous in-flight controller and increment a call id. When a promise settles, only resolve if its id matches the latest; otherwise reject with an AbortError. In an identity-guarded finally block, clear the stored controller only if it still belongs to that call. Completed calls are therefore not aborted later, and a stale completion cannot erase a newer in-flight controller.

1

Approach: AbortController + call id guard

Step-by-step:

1) Track a currentId and nullable currentController in the closure.

2) On every call, increment currentId and abort the stored controller only if that earlier call is still in flight.

3) Create a new AbortController and call fn(signal, ...args).

4) If the promise settles but its callId is not the latest, reject with an AbortError; otherwise forward the value or error.

5) In finally, clear currentController only when it is still the identical controller created by this call. This avoids retaining completed controllers and prevents stale completion from clearing a newer one.

Complexity: O(1) time and O(1) space per call.

function toAbortError(reason, message = 'Aborted') {
  if (reason && reason.name === 'AbortError') return reason;
  const err = new Error(reason && reason.message ? reason.message : message);
  try { err.name = 'AbortError'; } catch { try { Object.defineProperty(err, 'name', { value: 'AbortError' }); } catch {} }
  return err;
}

export default function takeLatest(fn) {
  let currentId = 0;
  let currentController = null;

  return function (...args) {
    currentId += 1;
    const callId = currentId;

    if (currentController && !currentController.signal.aborted) {
      try {
        currentController.abort(toAbortError(null, 'Cancelled'));
      } catch {
        // ignore
      }
    }

    const controller = new AbortController();
    currentController = controller;

    return Promise.resolve()
      .then(() => fn(controller.signal, ...args))
      .then(
        (value) => {
          if (callId !== currentId) throw toAbortError(null, 'Stale');
          return value;
        },
        (err) => {
          if (callId !== currentId) throw toAbortError(null, 'Stale');
          throw err;
        }
      )
      .finally(() => {
        if (currentController === controller) {
          currentController = null;
        }
      });
  };
}
function toAbortError(reason: any, message: string = 'Aborted'): Error {
  if (reason && reason.name === 'AbortError') return reason;
  const err = new Error(reason && reason.message ? reason.message : message);
  try { (err as any).name = 'AbortError'; } catch { try { Object.defineProperty(err as any, 'name', { value: 'AbortError' }); } catch {} }
  return err;
}

export default function takeLatest<TArgs extends any[], TResult>(
  fn: (signal: AbortSignal, ...args: TArgs) => Promise<TResult>
): (...args: TArgs) => Promise<TResult> {
  let currentId = 0;
  let currentController: AbortController | null = null;

  return function (...args: TArgs): Promise<TResult> {
    currentId += 1;
    const callId = currentId;

    if (currentController && !currentController.signal.aborted) {
      try {
        currentController.abort(toAbortError(null, 'Cancelled'));
      } catch {
        // ignore
      }
    }

    const controller = new AbortController();
    currentController = controller;

    return Promise.resolve()
      .then(() => fn(controller.signal, ...args))
      .then(
        (value) => {
          if (callId !== currentId) throw toAbortError(null, 'Stale');
          return value;
        },
        (err) => {
          if (callId !== currentId) throw toAbortError(null, 'Stale');
          throw err;
        }
      )
      .finally(() => {
        if (currentController === controller) {
          currentController = null;
        }
      });
  };
}

Notes & Pitfalls

Pitfalls
  • Forgetting to abort the previous controller allows stale requests to continue.
  • If the worker ignores AbortSignal, you still need a call id guard to prevent stale updates.
  • Not normalizing abort errors can lead to inconsistent error handling in tests and UI code.
  • Retaining a completed call's controller makes an unrelated later call abort already-finished work; clear it in an identity-guarded finally.
Edge cases
  • Multiple rapid calls in the same tick should still resolve only the latest one.
  • A previous call may resolve after being aborted; it must be ignored.
  • AbortController can have an undefined reason; create a fallback AbortError.
  • A stale call may finish after a newer call starts; its finally block must not clear the newer controller.
Techniques
  • AbortController for cancellation wiring.
  • Monotonic call id to ignore stale results.
  • Error normalization to standardize AbortError handling.
  • Identity-guarded controller cleanup in finally.

Common mistakes on this challenge

  • takeLatest cancellation sequencing is incorrect

    Only the most recent invocation should resolve, and only a previous call that is still in flight should be aborted.

    • Track active call id and `AbortController` for the latest invocation.
    • Abort previous controller before starting a new call.
    • Reject stale results/errors when call id no longer matches latest id.
    • In finally, clear the stored controller only when it is identical to this call’s controller.

Resources

  • MDN – AbortController
  • MDN – AbortSignal

Similar questions

Fetch JSON with Timeout + AbortintermediatePoll Until (with Timeout + Abort)intermediateAbortable Helpers (Timeout + Composed Abort)hard

Guides

Frontend interview preparation guideGuideFrontend coding interview questions and prep guideBlueprintJavaScript Problems That Actually Show UpBlueprintBuild Great UI in 60 MinutesBlueprint

Preparing for interviews? Use Frontend Coding Challenges first, then move into a concrete Study Plan before targeted Company Prep.

Open frontend interview questionsBrowse JavaScript interview questionsOpen Essential 60Open Machine Coding HubOpen Frontend Coding ChallengesOpen System DesignOpen Interview Prep GuideOpen System Design BlueprintOpen Framework Prep PathsOpen Study PlansOpen JavaScript mastery study planOpen Company PrepOpen JavaScript Framework Prep Guide
↗Incidents hub
32 / 88