Share a Fetch Request Without Sharing Cancellation

A single AbortSignal controls the underlying fetch(), so sharing that signal also shares cancellation. Give each caller a wrapper promise and keep a waiter count. Abort the network request only when no callers remain.

Last updated: October 30, 2026.

const inflight = new Map();

function getJson(url, { signal } = {}) {
  let entry = inflight.get(url);
  if (!entry) {
    const controller = new AbortController();
    const promise = fetch(url, { signal: controller.signal })
      .then(response => {
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
      })
      .finally(() => inflight.delete(url));
    entry = { controller, promise, waiters: 0 };
    inflight.set(url, entry);
  }

  entry.waiters += 1;
  let released = false;
  const release = () => {
    if (released) return;
    released = true;
    entry.waiters -= 1;
    if (entry.waiters === 0 && inflight.get(url) === entry) {
      entry.controller.abort();
    }
  };

  return new Promise((resolve, reject) => {
    const abort = () => {
      release();
      reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
    };
    if (signal?.aborted) return abort();
    signal?.addEventListener('abort', abort, { once: true });
    entry.promise.then(resolve, reject).finally(() => {
      signal?.removeEventListener('abort', abort);
      release();
    });
  });
}

Two callers using the same URL receive the same parsed result, but aborting one wrapper rejects only that caller. When the last waiter cancels, the shared controller stops the underlying request.

Separate caller cancellation from transport cancellation

MDN’s Fetch cancellation documentation explains that aborting the controller rejects the fetch with an AbortError. The AbortSignal reference also notes that a signal is one-use, so do not retain an aborted entry for later callers.

The wrapper removes its event listener and releases its waiter exactly once. The identity check prevents an old completion handler from aborting a newer request stored under the same URL.

Choose a safe deduplication key

A URL alone is sufficient only for identical GET requests. Include the method, relevant headers, credentials mode, tenant scope, and request body when those values change the response. Never share authenticated results across security boundaries.

The map above stores in-flight work, not completed data. Continue with calling routes with Fetch, debouncing repeated requests, and preventing duplicate API operations.

Related Web Cheat Sheet guides

Sergey Kornilov

Sergey Kornilov