How do you abort a web request using `AbortController` in JavaScript?
TL;DR
Create an AbortController, pass its signal to fetch(), and call controller.abort() when the result is no longer needed. fetch() and response-body consumption reject when aborted. Treat cancellation as an expected control-flow outcome, clean up any related timers or listeners, and create a new controller for the next operation because an aborted signal stays aborted.
Aborting releases the client from waiting and may cancel network activity, but it does not guarantee that the server stops or rolls back work already started. Make important writes idempotent or provide an application-level cancellation protocol.
One cancellation signal, multiple consumers
An AbortSignal can coordinate cancellation across every operation that was started with that signal.
Aborting rejects pending fetches with an abort-related error; it does not roll back work that the server has already completed.
Basic request cancellation
async function loadProfile(userId, { signal } = {}) {const response = await fetch(`/api/users/${encodeURIComponent(userId)}`, {signal,});if (!response.ok) {throw new Error(`Profile request failed (${response.status})`);}return response.json();}function startProfileLoad(userId) {const controller = new AbortController();return {promise: loadProfile(userId, { signal: controller.signal }),signal: controller.signal,cancel: () => controller.abort(),};}const operation = startProfileLoad('42');const cancelButton = document.querySelector('#cancel-profile');cancelButton?.addEventListener('click', operation.cancel, { once: true });try {const profile = await operation.promise;console.log(profile);} catch (error) {if (operation.signal.aborted) {console.log('Profile request was canceled');} else {throw error;}} finally {cancelButton?.removeEventListener('click', operation.cancel);}
The owning screen can also call operation.cancel() during disposal.
Latest-request-wins search
Typeahead search can abort an older request before starting the next one:
let activeController;async function search(query) {activeController?.abort();const controller = new AbortController();activeController = controller;try {const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {signal: controller.signal,});if (!response.ok) throw new Error(`Search failed (${response.status})`);const results = await response.json();if (activeController === controller) renderResults(results);} catch (error) {if (!controller.signal.aborted) showSearchError(error);} finally {if (activeController === controller) activeController = undefined;}}function disposeSearch() {activeController?.abort();activeController = undefined;}
The identity check is useful defense in depth when some dependency does not honor the signal or a result has already reached application code.
Adding a timeout
When a runtime or support policy does not provide the desired timeout helper, abort with a timer and clear it in finally:
async function fetchWithTimeout(url, timeoutMs) {const controller = new AbortController();const timeoutId = setTimeout(() => controller.abort(), timeoutMs);try {return await fetch(url, { signal: controller.signal });} finally {clearTimeout(timeoutId);}}
A timeout is not a retry policy. Retry only operations that are safe to repeat, use backoff and limits, and preserve the original deadline or caller cancellation requirements.
Important details
- One signal can cancel a group of operations, listeners, or streams that accept it. After it aborts, it cannot be reset.
abort(reason)can supply a reason in supporting APIs. Checkingsignal.abortedis more robust than assuming every cancellation rejects with an error named exactlyAbortError.- Aborting after
fetch()fulfills can still interrupt readingresponse.json(),response.text(), or a streamed body. - Cancellation is cooperative for APIs that merely receive a signal; custom operations must listen for
abort, stop work, and remove their listener. - In Node.js, many built-in asynchronous APIs also accept
AbortSignal, but support is API-specific.