Bitlyst

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.

AbortController & AbortSignal


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 */ }

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 with AbortError.


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 read signal.reason in custom code.

9) Quick Cheatsheet

NeedDo this
Cancel slow fetchfetch(url, { signal: AbortSignal.timeout(ms) })
Cancel on unmountCreate AbortController in useEffect, abort in cleanup
Cancel prior request (search)Keep last controller in ref, abort before new fetch
Cancel a batchShare one controller across requests and call abort()
Keep “why” it was cancelledcontroller.abort('reason'); signal.reason

Happy cancelling ✨ Use AbortController to keep your apps snappy, correct, and memory‑leak free.

How did you like this post?

👍0
❤️0
🔥0
🤔0
😮0
AbortController & AbortSignal · Bitlyst