AbortController & AbortSignal
Everything you need to cancel async work in JS: fetch, streams, custom tasks, React patterns, Node notes, timeouts, races, and common pitfalls.
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.signalto 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.
fetchwill 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.reasonin 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.
You made it to the end!
Did this help? Leave a reaction — it takes one second.
Got feedback? 💬
Typo, suggestion, question — I read every message.
Comments
Writing bite-sized JS, React & Next.js tips
Related
Get new posts in your inbox
No spam. Unsubscribe any time.