Request Lifecycle Control
UIMapFetch30s

AbortController

Cancel stale work by hand, let timeouts cancel the rest, and always retry with a fresh signal.

Interactive Demo

One run shows manual aborts and a 30-second auto-timeout

Start the simulation, cancel any request manually if you want, or leave the export request alone and it will abort itself at 30 seconds.

Controller Layer
idle
`jobControllers`Map(0)
// live controllers appear here
Event Log

// press "Start Demo" to populate the controller log

App + Worker UI
Manual Jobseach job can cancel

Invoice OCR

doc-41

idle

PDF Chunking

doc-43

idle
Timeout-Only Request
manual + timeout

Vendor Export

exp-77

waiting
user can abort now, timeout still existsidle
Implementation

How to build it after the demo

The key retry rule is simple: abort the old attempt, then retry with a completely new controller.

async function fetchWithRetry(url: string) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    const controller = new AbortController()
    const timeoutId = setTimeout(() => {
      controller.abort()
    }, 5000)

    try {
      const response = await fetch(url, {
        signal: controller.signal,
      })

      clearTimeout(timeoutId)
      return await response.json()
    } catch (error) {
      clearTimeout(timeoutId)

      if (attempt === 3) throw error

      // important: next attempt gets a new controller
      await delay(attempt * 400)
    }
  }
}

Fresh Signal Per Retry

Never reuse an aborted signal. Every retry attempt must allocate a brand new controller.

AbortError Is Normal

Treat cancellation as control flow. It should usually skip alerts and silently exit the current path.

Combine Sources

You can combine user cancel, route changes, and timeouts into one signal so downstream code handles only one abort path.