AbortController

Med

AbortController provides a standard mechanism to cancel asynchronous operations. It creates an AbortSignal that can be passed to fetch, event listeners, and custom async functions. This pattern is essential for preventing memory leaks in React useEffect, implementing request timeouts, and cancelling stale API calls.

Interactive Visualization

DOM Events

document>
body>
div>
button
Phase: Capture
Event travels DOWN from document to target

Key Points

  • AbortController creates a signal; calling controller.abort() triggers cancellation
  • Fetch accepts a signal option — aborting throws an AbortError that must be caught
  • AbortSignal.timeout(ms) creates a signal that auto-aborts after a duration
  • AbortSignal.any([signal1, signal2]) combines multiple signals — first abort wins
  • Event listeners accept a signal option for automatic removal on abort
  • Essential in React useEffect cleanup to prevent state updates on unmounted components

Code Examples

Aborting a Fetch Request

const controller = new AbortController()

fetch('https://api.example.com/data', {
  signal: controller.signal,
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request was cancelled')
    } else {
      console.error('Fetch failed:', err)
    }
  })

setTimeout(() => controller.abort(), 100)

The signal is passed to fetch. When abort() is called, fetch rejects with an AbortError.

Timeout with AbortSignal.timeout

async function fetchWithTimeout(url, timeoutMs) {
  try {
    const response = await fetch(url, {
      signal: AbortSignal.timeout(timeoutMs),
    })
    return await response.json()
  } catch (err) {
    if (err.name === 'TimeoutError') {
      console.log('Request timed out')
    } else if (err.name === 'AbortError') {
      console.log('Request was aborted')
    } else {
      throw err
    }
  }
}

fetchWithTimeout('https://api.example.com/slow', 5000)

AbortSignal.timeout() auto-aborts after the specified duration. The error is a TimeoutError.

React useEffect Cleanup Pattern

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    const controller = new AbortController()

    async function loadUser() {
      try {
        const res = await fetch('/api/users/' + userId, {
          signal: controller.signal,
        })
        const data = await res.json()
        setUser(data)
      } catch (err) {
        if (err.name !== 'AbortError') {
          console.error('Failed to load user:', err)
        }
      }
    }

    loadUser()
    return () => controller.abort()
  }, [userId])
}

The cleanup function aborts in-flight requests, preventing state updates on unmounted components.

Event Listener Removal with Signal

const controller = new AbortController()

window.addEventListener('resize', handleResize, {
  signal: controller.signal,
})
window.addEventListener('scroll', handleScroll, {
  signal: controller.signal,
})

// One call removes ALL listeners
controller.abort()

Passing a signal to addEventListener lets you remove multiple listeners with a single abort() call.

Common Mistakes

  • Forgetting to catch AbortError — unhandled promise rejection crashes the app
  • Creating the AbortController outside useEffect — must be created fresh each time
  • Reusing an already-aborted controller — once aborted, it stays aborted
  • Not distinguishing AbortError from TimeoutError when using AbortSignal.timeout()

Interview Tips

  • Controller owns the trigger, signal is the read-only token passed to consumers
  • Demonstrate the React useEffect cleanup pattern — practical memory leak prevention
  • AbortSignal.any() for combining user cancellation with timeout signals
  • addEventListener with signal is the modern replacement for removeEventListener

Related Concepts