Topics
v5.0.0
On this page · sections

JavaScript Error Handling & Cancellation

Can you fail with a useful error, stop waiting, and actually cancel the underlying work?

try/catch is not a strategy. Interviews want custom errors, Promise rejection rules, timeouts, and AbortController.

Timeout wrappers and retry sit on top of a scheduler in Task Scheduler. This page is the language and host contract.

Questions

Errors

  1. Throw, Reject, and Custom Errors
  2. fetch Does Not Reject on HTTP Errors

Cancellation

  1. Timeout vs Abort
  2. AbortController and Signals

Senior-Level Follow-ups

  1. Ignoring Late Resolves
  2. useEffect Cleanup
  3. What Should Be Retryable?

Errors

1. Throw, Reject, and Custom Errors

Why This Matters

throw "string" loses stack traces. Swallowing errors in catch hides production failures.

Problem

How should errors propagate in sync code and in Promises?

Example

class AppError extends Error { constructor(code, message) { super(message); this.name = "AppError"; this.code = code; } } throw new AppError("UNAVAILABLE", "payments down");

Sync throw unwinds the stack until catch.

Async:

async function load() { throw new AppError("NETWORK", "offline"); } load().catch((err) => { if (err instanceof AppError) return show(err); throw err; });

Throwing inside then rejects the next Promise. try/catch around await is the same.

Interview Answer

“I throw Error subclasses with a name and code. Sync errors unwind; async errors reject. I catch at a boundary, log, and rethrow unknown errors. I never throw strings.”

2. fetch Does Not Reject on HTTP Errors

Why This Matters

retry(fetch) will not retry a 500 unless you throw.

Problem

When does fetch reject, and how do you turn HTTP failure into an exception?

Example

const response = await fetch("/api/users"); if (!response.ok) { throw new AppError("HTTP", `HTTP ${response.status}`); } return response.json();

fetch rejects on network failure or abort — not on 404/500.

response.json() can still throw on invalid JSON.

Interview Answer

“fetch fulfills with a Response for HTTP errors. I check ok (or status) and throw. That keeps retry and UI error paths uniform.”

Cancellation

3. Timeout vs Abort

Why This Matters

Promise.race against a timer only stops waiting.

Problem

What is the difference between a timeout wrapper and cancelling fetch?

Example

Timeout → stop waiting; work may continue Abort → tell the host to stop the operation
Promise.race([ fetch("/api"), sleep(3000).then(() => { throw new Error("timeout"); }), ]);

The request can still complete and invoke handlers unless you abort.

Always clearTimeout when the operation wins, or the timer leaks a rejection.

Interview Answer

“A timeout is a competing Promise. Cancellation is a signal to the operation. For fetch, I pass AbortSignal and abort on timeout. Race alone is not enough.”

4. AbortController and Signals

Why This Matters

One signal can cancel fetch, streams, and event listeners together.

Problem

How do you wire abort through an async function?

Example

const controller = new AbortController(); const response = await fetch("/api/data", { signal: controller.signal, }); controller.abort();

Aborted fetch rejects with AbortError (DOMException). Callers should treat abort as control flow, not as a toast-worthy failure.

el.addEventListener("click", onClick, { signal: controller.signal });

Nested work should accept signal and forward it. Do not create a new controller per child unless you need independent cancel.

Interview Answer

“I create an AbortController, pass signal into fetch and listeners, and abort from timeout, unmount, or a new request. AbortError is expected. I forward the same signal through the call chain.”

Senior-Level Follow-ups

5. Ignoring Late Resolves

A cancelled request may still resolve. Guard UI updates:

let generation = 0; async function search(q) { const id = ++generation; const data = await fetchResults(q); if (id !== generation) return; setResults(data); }

Or abort the previous controller so the network work stops too.

6. useEffect Cleanup

useEffect(() => { const controller = new AbortController(); load(controller.signal); return () => controller.abort(); }, [id]);

Cleanup must abort. Otherwise Strict Mode double-mount and route changes leak requests and setState on unmounted trees.

7. What Should Be Retryable?

Retry transient failures: network, 429, 503.

Do not retry 400, 401, 403, or AbortError.

Retries are sequential with delay. Parallel retries amplify load. Details: Task Scheduler.

Final Interview Strategy

1. Throw Error subclasses 2. Catch at boundaries 3. fetch needs an ok check 4. Timeout ≠ cancel 5. AbortSignal is the cancel token 6. Ignore stale responses 7. Abort on unmount; don't retry AbortError

Senior-level signal: Separate “the Promise settled” from “the operation stopped.” Production async code needs both.

Last updated on