AceDevHub
Async JavaScriptFree

Error Handling in Async Code

Handle failures consistently with try/catch, centralized handlers, custom errors, and patterns that survive production load.

IntermediateFreeAsync

Async errors do not crash the process like uncaught sync throws — they become rejected promises that silently fail if nobody attaches catch or await try/catch. Production APIs need consistent error shapes, logged stack traces, and user-safe messages. This lesson connects promise rejection mechanics to how AceDevHub-style Fastify handlers and frontend fetch wrappers should behave.

Unhandled rejections — the silent killer

Node emits unhandledRejection when a promise rejects with no catch by the time microtasks drain. Browsers log similar warnings. In servers, this correlates with 500 responses never sent and hung requests. Rule: every async entry point (HTTP handler, queue job, event listener) must have a boundary that catches and maps errors.

unhandled.js
Loading editor…
Outputconsole
Handled: expected failure

Custom Error classes — typed failures

Extend Error with named subclasses (ValidationError, NotFoundError) so handlers map status codes without string matching. Set this.name and use instanceof at boundaries. Keep message user-safe; log internal details separately. TypeScript can narrow on name or custom fields for API envelopes.

custom-errors.js
Loading editor…
Outputconsole
{ status: 404, body: { error: 'User not found' } }
{ status: 500, body: { error: 'Internal server error' } }

Wrapping and rethrowing — preserve context

Low-level errors (ECONNREFUSED, JSON parse fail) should gain context when bubbling up: throw new Error(`Failed to load user ${id}: ${err.message}`, { cause: err }) (ES2022 cause chain). Log the full cause; return sanitized messages to clients. Never expose stack traces in public API JSON.

cause-chain.js
Loading editor…
Outputconsole
getUser(42) failed
Cause: connection reset

Retry with backoff — transient failures

Network blips and 503 responses often succeed on retry. Exponential backoff (wait 100ms, 200ms, 400ms…) plus jitter avoids thundering herd. Cap max attempts and only retry idempotent operations or safe reads — retrying POST payments without idempotency keys causes duplicate charges.

retry.js
Loading editor…
Outputconsole
success

Chapter 4 checkpoint

You understand why setTimeout runs after sync code, how promises represent future values, how async/await reads sequentially while staying non-blocking, how combinators orchestrate parallel work, and how to handle errors at system boundaries. Chat 5 closes the track with modern syntax utilities, fetch patterns, debounce/throttle, cloning, and engineering best practices.

AceDevHub Fastify handlers wrap service calls in try/catch and map known errors to structured API envelopes while logging cause chains server-side. Frontend fetch wrappers mirror the same discipline — never assume catch handles HTTP 404 because fetch only rejects on network failure.

  1. 1Event loop: stack → microtasks → macrotasks
  2. 2Promises chain; async/await is sugar on top
  3. 3all vs allSettled vs race vs any — pick by failure policy
  4. 4Custom errors + cause for debuggable production logs
  5. 5Next: optional chaining, fetch, debounce, best practices