FrontendAtlas
Interview Questions
Home>JavaScript interview questions> JavaScript coding challenges >Fetch JSON with Timeout + Abort

Fetch JSON with Timeout + Abort

intermediate
By FrontendAtlas Editorial · Updated Jul 14, 2026
Implement fetchJson(url, options) that fetches JSON with built-in timeout and abort support. It should abort on timeout, respect an external AbortSignal, and throw on non-2xx HTTP responses. This mirrors real-world client utilities for APIs. Concepts: fetch, AbortController, timeouts, error handling.

Arguments

  • url: string — The request URL.
  • options: { timeoutMs?: number, signal?: AbortSignal, fetchFn?: Function, ...init } — Optional timeout, AbortSignal, and a fetch function override for testing. Any extra fields are passed to fetch.

Returns

Promise<any> — Resolves with parsed JSON or rejects on timeout, abort, or non-OK response.
Examples
// Basic usage
const data = await fetchJson('/api/user/42', { timeoutMs: 2000 });

// With external cancellation
const ac = new AbortController();
const p = fetchJson('/api/search?q=js', { signal: ac.signal });
ac.abort();
await p; // rejects with AbortError

Solution

Overview

Create a new AbortController per call. If an external signal aborts, forward that to the controller. If a timeout is set, abort after the deadline. Call fetch, throw on non-OK status, and return response.json(). Always clear timers and listeners.

1

Approach: AbortController + timeout guard

Steps:

1) Extract timeoutMs, signal, and fetchFn from options.

2) Create an AbortController and forward external aborts.

3) If timeoutMs is set, abort with a Timeout AbortError.

4) Call fetch with the combined signal.

5) Throw on non-OK status.

6) Parse and return JSON.

7) Always cleanup listeners and timers.

Complexity: O(1) extra work beyond the network request.

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 async function fetchJson(url, options = {}) {
  const { timeoutMs, signal, fetchFn, ...init } = options;
  const fetcher = fetchFn || (typeof fetch === 'function' ? fetch : null);
  if (!fetcher) throw new Error('fetchFn is required');

  const controller = new AbortController();
  let timeoutId;

  const onAbort = () => {
    controller.abort(toAbortError(signal && signal.reason));
  };

  if (signal) {
    if (signal.aborted) {
      controller.abort(toAbortError(signal.reason));
    } else {
      signal.addEventListener('abort', onAbort, { once: true });
    }
  }

  if (typeof timeoutMs === 'number' && timeoutMs >= 0) {
    timeoutId = setTimeout(() => {
      controller.abort(toAbortError(null, 'Timeout'));
    }, timeoutMs);
  }

  try {
    if (controller.signal.aborted) {
      throw toAbortError(controller.signal.reason);
    }

    const res = await fetcher(url, { ...init, signal: controller.signal });
    if (!res || !res.ok) {
      const status = res && typeof res.status === 'number' ? res.status : 0;
      throw new Error(`HTTP ${status}`);
    }
    return await res.json();
  } finally {
    if (timeoutId) clearTimeout(timeoutId);
    if (signal) signal.removeEventListener('abort', onAbort);
  }
}
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;
}

type FetchJsonOptions = {
  timeoutMs?: number;
  signal?: AbortSignal;
  fetchFn?: (input: string, init?: any) => Promise<{ ok: boolean; status: number; json: () => Promise<any> }>;
  [key: string]: any;
};

export default async function fetchJson<T = any>(
  url: string,
  options: FetchJsonOptions = {}
): Promise<T> {
  const { timeoutMs, signal, fetchFn, ...init } = options;
  const fetcher = fetchFn || (typeof fetch === 'function' ? fetch : null);
  if (!fetcher) throw new Error('fetchFn is required');

  const controller = new AbortController();
  let timeoutId: ReturnType<typeof setTimeout> | undefined;

  const onAbort = () => {
    controller.abort(toAbortError(signal && (signal as any).reason));
  };

  if (signal) {
    if (signal.aborted) {
      controller.abort(toAbortError((signal as any).reason));
    } else {
      signal.addEventListener('abort', onAbort, { once: true });
    }
  }

  if (typeof timeoutMs === 'number' && timeoutMs >= 0) {
    timeoutId = setTimeout(() => {
      controller.abort(toAbortError(null, 'Timeout'));
    }, timeoutMs);
  }

  try {
    if (controller.signal.aborted) {
      throw toAbortError((controller.signal as any).reason);
    }

    const res = await fetcher(url, { ...init, signal: controller.signal });
    if (!res || !res.ok) {
      const status = res && typeof res.status === 'number' ? res.status : 0;
      throw new Error(`HTTP ${status}`);
    }
    return await res.json();
  } finally {
    if (timeoutId) clearTimeout(timeoutId);
    if (signal) signal.removeEventListener('abort', onAbort);
  }
}

Notes & Pitfalls

Pitfalls
  • Using Promise.race without AbortController does not cancel the fetch.
  • Forgetting to clear timeouts or abort listeners can leak resources.
  • Not checking response.ok can silently treat HTTP 404/500 as success.
Edge cases
  • signal already aborted: reject immediately.
  • timeoutMs = 0 should timeout immediately.
  • fetchFn not provided and fetch missing: throw a helpful error.
Techniques
  • AbortController for cancellation.
  • Timeout guard with setTimeout.
  • Defensive error handling for non-OK responses.

Resources

  • MDN – fetch()
  • MDN – AbortController

Similar questions

Take Latest (Abort Previous Requests)intermediatePoll Until (with Timeout + Abort)intermediateStream to Text (ReadableStream -> string)intermediate

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
13 / 88