Prompt and context
A search page concurrently requests suggestions, profile data, and results. A new query or navigation should cancel old work quickly; a stalled network request should time out, but returning from the background must not count frozen time as online request time. Design cancellation with AbortSignal.timeout(), AbortSignal.any(), and AbortController.
This fits frontend, Web performance, and asynchronous-architecture interviews. The key is separating user aborts, timeouts, page lifecycle, and real network failures instead of showing every exception as “request timed out.”
What the interviewer is testing
A strong answer says AbortSignal.timeout() returns a signal that aborts automatically with a TimeoutError; a user or controller abort is usually an AbortError. It also explains active-time semantics: the timer pauses while a page is in bfcache or a Worker is suspended. Multiple signals can be combined with AbortSignal.any() while preserving a diagnosable reason.
Clarifying questions to ask first
- Which requests are safe to cancel, and must writes complete or be queried later?
- Does timeout start at user action, request dispatch, or page visibility?
- Do user abort, unmount, navigation, and timeout need different messaging?
- Do target browsers support the static methods, and should fallback keep a manual timer?
- How are retries, deduplication, caching, and result races handled?
A 30-second answer framework
“For each request I would combine user cancellation with AbortSignal.timeout() and classify TimeoutError, AbortError, and network errors separately. Timeout uses active time, so suspension or bfcache should not be reported as online latency. A new query cancels the old controller, and result submission also checks a request sequence or signal so stale data cannot win. Older browsers get a controller-plus-timer fallback, tested for cancellation, timeout, resume, and races.”
Step-by-step deep answer
Step 1: Separate three cancellation sources
AbortController.abort() is application-initiated; AbortSignal.timeout(ms) aborts when active time reaches a limit; AbortSignal.any([...]) fires when any input signal aborts. All can be passed to fetch, but the business layer should retain the reason so leaving a page is not logged as a service failure.
const controller = new AbortController();
const timeout = AbortSignal.timeout(5000);
const signal = AbortSignal.any([controller.signal, timeout]);
fetch(url, { signal });Step 2: Classify by reason
A timeout uses a TimeoutError DOMException; a user cancellation or browser stop commonly uses AbortError. DNS, connection reset, and CORS problems may appear as other errors. Catching code should inspect signal.reason or the exception name before choosing messaging, retry, and log severity.
Step 3: Understand active time
timeout() measures active time rather than a simple wall clock. The documentation notes that the timer pauses while a page is in bfcache or a Worker is suspended. That suits a user-perceived active request window, not an absolute server deadline; the server still needs its own timeout and idempotency policy.
Step 4: Handle search races
When the user types repeatedly, abort the old controller before creating a signal for the new query. Even if old network work has completed, its callback may still be queued; check request sequence, current query, or signal.aborted before committing state. Cancellation alone does not provide result-version ordering.
Step 5: Separate cancellable reads and writes
Search, image, and suggestion reads are usually cancellable. Payment, order, or upload writes may already have taken effect on the server. Cancelling client waiting does not undo a server side effect. Use idempotency keys, status queries, or a background task for writes instead of simply choosing a shorter timeout.
Step 6: Design signal lifetimes
Abort a component controller on unmount, reuse a page-level signal on navigation, and add a request timeout for the individual operation. Never share a permanently aborted global signal with later requests; create a fresh signal per operation. Give different sources explicit reasons when diagnostics matter.
Step 7: Provide a compatibility fallback
If an older browser lacks AbortSignal.timeout or any, combine AbortController with setTimeout, clear the timer, and normalize the reason. Feature detection belongs at runtime, and the default path must still handle environments without the static methods.
Step 8: Verify lifecycle and cleanup
Test rapid input, unmount, navigation, background suspension, bfcache restore, timeout, user abort, network failure, and repeated retries. Confirm that the request is cancelled, timers are cleared, stale results cannot commit, messages are accurate, and catching the exception does not create unhandled promises or state-update warnings.
Trade-offs and boundaries
Merged signals reduce cancellation glue, but they do not make a server transaction reversible or guarantee that a response never arrives. Active-time semantics fit user experience but not an end-to-end SLA. Set timeout values by request type, network conditions, and retry budget, coordinating with the server deadline.
Do not use only Promise.race to simulate a timeout and leave the underlying request running. Pass an AbortSignal so the network and resources can stop. Do not log every abort as an error, or normal navigation will pollute alerts.
Rollout plan and evidence
Start with a request state machine for search reads: create signal, dispatch, classify reason, commit a versioned result, and abort on unmount. Record request type, reason, active duration, retry count, and final state without sensitive query text.
Compare supported and fallback browsers across bfcache, Workers, slow networks, and rapid input. Acceptance requires no stale result overwrite, no timer leak, accurate cancellation messaging, no accidental write abort, and evidence of server idempotency or status querying.
Common pitfalls and follow-ups
Conflating TimeoutError and AbortError
User abort, unmount, and timeout have different next steps. Classify by reason to choose messaging, logging, and retry.
Assuming client abort rolls back a server write
The request may already have reached the server. Writes need an idempotency key, status query, or compensation flow.
Replacing real cancellation with Promise.race
Racing changes what the caller waits for but does not stop the underlying fetch. Pass AbortSignal and clean up resources.
Ignoring active time around bfcache
The timeout may pause while the page is suspended. Do not treat post-restore duration as online request SLA.
How do you prevent an old result from winning?
Aborting the old controller is not enough. Compare a query sequence, version, or current query before committing so response order cannot change state.