DEV Community

Mohsen Fallahnejad
Mohsen Fallahnejad

Posted on

AbortController & AbortSignal

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 an AbortError (or signal.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 */ } 
Enter fullscreen mode Exit fullscreen mode

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') 
Enter fullscreen mode Exit fullscreen mode

Tip: If you pass a reason, propagate it in your own tasks. fetch will still reject with AbortError.


3) Fetch + Timeouts

A) Easiest: AbortSignal.timeout(ms)

// Modern browsers & Node 18+ const res = await fetch('/slow', { signal: AbortSignal.timeout(3000) }) 
Enter fullscreen mode Exit fullscreen mode

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) } 
Enter fullscreen mode Exit fullscreen mode

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')) 
Enter fullscreen mode Exit fullscreen mode

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 }) }) } 
Enter fullscreen mode Exit fullscreen mode

Propagate reason:

const onAbort = () => reject(signal.reason ?? new DOMException('Aborted', 'AbortError')) 
Enter fullscreen mode Exit fullscreen mode

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() 
Enter fullscreen mode Exit fullscreen mode

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]) 
Enter fullscreen mode Exit fullscreen mode

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) } } 
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

Usage:

const c = new AbortController() const s = anySignal(c.signal, AbortSignal.timeout(3000)) fetch('/x', { signal: s }) 
Enter fullscreen mode Exit fullscreen mode

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 read signal.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)