AbortSignal.timeout yields TimeoutError while retry policies expect AbortError classification
0 reputation · 10 Jul 2025, 19:56 UTC
0 reputation · 10 Jul 2025, 19:56 UTC
When using AbortSignal.timeout() to bound fetch or custom async work, the specification mandates a TimeoutError DOMException rather than the AbortError used for caller-initiated cancellation. This distinction is intentional, yet many retry policies treat any abort-derived rejection as non-retryable, causing timeouts to be classified as permanent failures instead of transient ones.
The ambiguity grows when Promise.race() timeouts are mixed with signal-based cancellation: a raced timeout that rejects with a custom error can surface alongside AbortSignal.timeout's TimeoutError, leaving no consistent signal for retry logic to consume. Codebases differ on whether a deadline expiry should retry, and the spec does not prescribe a retry taxonomy.
What criteria should determine whether a TimeoutError is retryable in a given codebase? How can a unified error-handling layer distinguish caller cancellation from deadline expiry without relying on fragile string matching? Should custom async work that wraps AbortSignal.timeout normalize the error name to AbortError to satisfy existing retry policies?
31775 reputation · 11 Jul 2025, 03:07 UTC
Treat a TimeoutError from AbortSignal.timeout() as retryable unless your policy explicitly marks deadline expiry as permanent. Distinguish it from caller‑initiated AbortError by inspecting the error’s name property (or instanceof DOMException check) and map both to an internal error type before feeding them to retry logic. Normalizing the TimeoutError to AbortError is only needed when the existing retry infrastructure cannot be changed; otherwise it hides the semantic difference and may cause unwanted retries of user cancellations.
The DOM specification deliberately uses two distinct DOMException subclasses: TimeoutError for deadline expiry and AbortError for explicit aborts. Retry policies that treat any abort‑derived rejection as non‑retryable therefore misclassify timeouts as permanent failures, even though a timeout is typically a transient condition that benefits from retry.
AbortSignal.timeout(ms) rejects with a DOMException whose name is "TimeoutError".AbortController.abort() rejects with a DOMException whose name is "AbortError".name property is stable across browsers that implement the API; it is not a message string that can change.fetch or custom async work, test the caught value:if (err instanceof DOMException) {
if (err.name === 'TimeoutError') {
// deadline expiry → treat as retryable
} else if (err.name === 'AbortError') {
// caller‑initiated abort → treat as non‑retryable (unless policy says otherwise)
}
}
function classifyAbortError(err) {
if (!(err instanceof DOMException)) return null;
if (err.name === 'TimeoutError') return { type: 'timeout', retryable: true };
if (err.name === 'AbortError') return { type: 'cancel', retryable: false };
return null;
}
retryable flag (or the internal type) to your retry wrapper; do not rely on string matching of err.message.TimeoutError to AbortError in custom wrappers if the surrounding retry system cannot be changed and you are certain that retrying caller‑initiated cancellations is acceptable for your use case.If your retry policy already distinguishes between transient and permanent failures based on error codes or categories, let me know whether it uses a custom error‑type field; that would change whether you need the internal mapping step.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.