Bounding axios requests: timeout vs AbortController signal vs CancelToken
Choose a timeout backstop plus per-operation AbortController for axios requests. Compare timeout, signal, and deprecated CancelToken, with trade-offs and a latest-search-wins cancellation pattern.
14 Sept 2025, 14:55 UTC

The decision
\nAn application needs two different guarantees for in-flight HTTP requests: a hard deadline so a slow server cannot hang the UI, and cooperative cancellation when a newer user action makes the previous request irrelevant. In axios the supported ways to enforce those guarantees are a per-request or instance timeout, an AbortController signal passed via the request config, and the legacy CancelToken. New code should use timeout as a backstop and AbortController for lifecycle-aware cancellation.
\nConstraints that drive the choice are version support, error handling stability across axios 0.x and 1.x, and the need to avoid unmount or supersession race conditions where stale responses update state.
\nSupported options compared
\n| Mechanism | \nHow it is configured | \nWhen it triggers | \nTypical use | \n
|---|---|---|---|
| timeout | \naxios.create({ timeout: 8000 }) or per request { timeout: 8000 }. 0 disables. | \nElapsed time since request start, no response received within window. | \nDeclarative deadline, framework agnostic. | \n
| AbortController signal | \nsignal: controller.signal in AxiosRequestConfig. Supported since axios v0.22.0. | \ncontroller.abort() called by app code on unmount, route change, or newer input. | \nCooperative cancel, latest-search-wins, cancel on unmount. | \n
| CancelToken | \ncancelToken: source.token from axios.CancelToken.source(). Deprecated. | \nsource.cancel() called by app code. | \nMigration only for legacy codebases. | \n
Trade-offs
\nTimeout is one line and works everywhere axios runs. It cannot be triggered by UI events and it is fixed for the request. It is best as a safety net.
\nAbortController is cooperative and lifecycle aware. One controller can be shared across multiple requests, and aborting is immediate from app code. It requires wiring creation, storage, and cleanup per logical operation, and abort after a response has fully arrived is a no-op, so state updates still need a guard against races.
\nCancelToken is the pre-AbortController mechanism. Axios documentation marks it deprecated. New code should not adopt it; existing code should map token.cancel() to controller.abort().
\nCombining an instance-level timeout with per-operation signals gives both a deadline backstop and cooperative cancel. The timeout protects against network stalls; the signal handles supersession.
\nRecommended pattern
\nCreate one axios instance with a conservative default timeout. Create a fresh AbortController for each logical operation and abort the previous controller when the operation is superseded. Normalize errors in a response interceptor so callers see domain errors or a canceled flag only.
\nconst api = axios.create({ baseURL: '/api', timeout: 8000 });\nPer operation, keep a reference to the current controller and abort it before starting a new one.
\nlet ctl;\ninput.oninput = async () => {\n ctl?.abort();\n ctl = new AbortController();\n try {\n render((await api.get('/search', {\n params: { q: input.value },\n signal: ctl.signal\n })).data);\n } catch (e) {\n if (!axios.isCancel(e)) showError(e);\n }\n};\nThe instance timeout is the backstop; the signal handles supersession. Guard rendering by comparing against the latest controller if the UI allows parallel updates.
\nError handling
\nUse axios.isCancel(err) to detect cancellation in a version-stable way. Do not branch on raw err.code alone.
\nIn the 1.x line, aborted requests reject with code ERR_CANCELED while timeouts reject with ECONNABORTED. Older 0.x releases used different codes. Prefer isCancel for portable code and treat timeouts as operational errors.
\nAn example normalization interceptor:
\napi.interceptors.response.use(\n res => res,\n err => {\n if (axios.isCancel(err)) return Promise.reject({ canceled: true });\n if (err.code === 'ECONNABORTED') return Promise.reject({ timeout: true, cause: err });\n return Promise.reject(err);\n }\n);\nLimitations and verification
\nTimeout semantics can differ between the browser XHR adapter and the Node http adapter, including behavior while a large response body is still downloading. Verify against your axios version and payload profile.
\nAbortSignal.timeout(ms) is a platform capability of modern browsers and Node, not an axios feature. Its availability depends on the runtime, unlike axios's own timeout option.
\nPractical checks:
\n- \n
- Confirm installed version. Run npm ls axios in the project root with read permission. Expected check is the version string in package.json and the installed tree. Risk is mismatched versions between dev and production. \n
- Check TypeScript definitions for AxiosRequestConfig in your installed version to confirm signal and timeout keys and the exported CanceledError type. \n
- Write a small script that fires one request with timeout: 100 and another aborted via controller.abort(). Log axios.isCancel(err) and err.code to pin down behavior in your version. \n
- Exercise both adapters you ship with realistic payload sizes to confirm timeout and abort behavior end to end. \n
Do not present CancelToken as a current recommendation. New code should use AbortController signals.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.