AbortController
is the standard way to cancel async work in modern JavaScript. It pairs with AbortSignal, which you pass to tasks so they can stop immediately.
1) TL;DR
- Create a controller → pass
controller.signal
to your async work. - Call
controller.abort(reason?)
to cancel; consumers see anAbortError
(orsignal.reason
). - Works with
fetch
, streams, and your own functions.
const c = new AbortController()
const resP = fetch('/api/data', { signal: c.signal })
// later...
c.abort('user navigated away')
try { await resP } catch (e) { if (e.name === 'AbortError') /* ignore */ }
2) Core API (with reason
support)
const c = new AbortController()
const { signal } = c
signal.aborted // boolean
signal.reason // any (why it was aborted)
c.abort(new DOMException('Timeout', 'AbortError'))
// or: c.abort('User left the page')
Tip: If you pass a reason, propagate it in your own tasks.
fetch
will still reject withAbortError
.
3) Fetch + Timeouts
A) Easiest: AbortSignal.timeout(ms)
// Modern browsers & Node 18+
const res = await fetch('/slow', { signal: AbortSignal.timeout(3000) })
B) Manual timer
const c = new AbortController()
const id = setTimeout(() => c.abort(new DOMException('Timeout', 'AbortError')), 3000)
try {
const res = await fetch('/slow', { signal: c.signal })
// use res
} catch (e) {
if (e.name !== 'AbortError') throw e
} finally {
clearTimeout(id)
}
C) Race utilities
// winner-takes-all -> cancel the losers
const controllers = [new AbortController(), new AbortController()]
const [a, b] = controllers.map(c => fetch('/mirror', { signal: c.signal }))
const winner = await Promise.any([a, b])
controllers.forEach(c => c.abort('lost the race'))
4) Make Your Own Functions Abortable
export function wait(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms)
const onAbort = () => { clearTimeout(id); reject(new DOMException('Aborted', 'AbortError')) }
if (signal.aborted) return onAbort()
signal.addEventListener('abort', onAbort, { once: true })
})
}
Propagate reason:
const onAbort = () => reject(signal.reason ?? new DOMException('Aborted', 'AbortError'))
5) Streams & Readers (Browser + Node)
const c = new AbortController()
const res = await fetch('/stream', { signal: c.signal }) // can be aborted
const reader = res.body.getReader({ signal: c.signal }) // abort affects reads too
// later
c.abort()
Node: fetch
in Node 18+ also supports abort; for streams, pipe/reader operations should react to abort and close resources.
6) React Patterns
A) Cancel on unmount (and on deps change)
useEffect(() => {
const c = new AbortController()
;(async () => {
try {
const r = await fetch('/api/search?q=' + q, { signal: c.signal })
setData(await r.json())
} catch (e) {
if (e.name !== 'AbortError') console.error(e)
}
})()
return () => c.abort('component unmounted or q changed')
}, [q])
B) Latest-typed value wins (typeahead)
const ref = useRef<AbortController | null>(null)
async function onType(v: string) {
ref.current?.abort('superseded')
const c = new AbortController()
ref.current = c
try {
const r = await fetch('/api?q=' + v, { signal: c.signal })
setOptions(await r.json())
} catch (e) { if (e.name !== 'AbortError') console.error(e) }
}
7) Small Utilities (copy‑paste)
// create a controller that auto-aborts after ms
export const withTimeout = (ms = 5000) => AbortSignal.timeout(ms)
// combine multiple signals -> aborted if ANY aborts
export function anySignal(...signals) {
const c = new AbortController()
const onAbort = (s) => c.abort(s.reason ?? new DOMException('Aborted', 'AbortError'))
signals.forEach(s => s.addEventListener('abort', () => onAbort(s), { once: true }))
return c.signal
}
Usage:
const c = new AbortController()
const s = anySignal(c.signal, AbortSignal.timeout(3000))
fetch('/x', { signal: s })
8) Common Pitfalls & Gotchas
-
Not wiring the signal → pass
{ signal }
everywhere the task supports it. -
Forgetting cleanup → clear timers and remove listeners on abort (use
{ once: true }
). -
Swallowing all errors → only ignore
AbortError
; surface real failures. - Global controller reuse → create fresh controllers per operation to avoid accidental cross‑cancels.
-
Overriding reason → if you care about why, use
abort(reason)
and readsignal.reason
in custom code.
9) Quick Cheatsheet
Need | Do this |
---|---|
Cancel slow fetch | fetch(url, { signal: AbortSignal.timeout(ms) }) |
Cancel on unmount | Create AbortController in useEffect , abort in cleanup |
Cancel prior request (search) | Keep last controller in ref , abort before new fetch |
Cancel a batch | Share one controller across requests and call abort()
|
Keep “why” it was cancelled | controller.abort('reason'); signal.reason |
Happy cancelling ✨ Use AbortController to keep your apps snappy, correct, and memory‑leak free.
Originally published on: Bitlyst
Top comments (0)