Error handling checklist
The checklist
We have covered a lot of ground in this course. Before the capstone, let’s gather everything into a single checklist you can use as a reference. Go through each item and check whether your app follows it.
Error fundamentals
- Always throw Error objects, not strings or plain objects (when you do throw)
- Use the
causeproperty to chain errors with context - Always
awaitpromises inside try-catch - Return error responses for expected failures, reserve throwing for unexpected ones
Structured errors
- Error response helpers produce consistent
{ error: { code, message, details? } }format - Error classes extend a base AppError for service-level errors
- A global error handler catches unexpected throws and returns clean 500 responses
- Routes return errors directly for anticipated cases (validation, not found, conflict)
Error responses
- Every error response uses the same format:
{ error: { code, message, details? } } - Expected errors return specific status codes (400, 404, 409)
- Unexpected errors return generic 500 with no internal details exposed
- Stack traces never appear in HTTP responses
Logging
- Expected errors logged at info/warn level
- Unexpected errors logged at error level with full stack trace
- Structured JSON logging for machine parsing
- Errors include context: request path, method, user ID
Resilience
- External service calls have timeouts (never wait forever)
- Transient failures are retried with exponential backoff and jitter
- Only idempotent operations are retried
- Circuit breakers prevent calling services that are known to be down
- Non-critical operations have fallbacks (queue, cache, defaults)
Server lifecycle
- SIGTERM and SIGINT handled for graceful shutdown
- Shutdown stops new connections, waits for in-flight requests, then exits
- Shutdown has a timeout (forced exit if cleanup takes too long)
-
uncaughtExceptionlogs and shuts down -
unhandledRejectionlogs the error - Health check reflects dependency health (not just “ok”)
Classification
- Dependencies classified: critical, important, nice-to-have
- Critical dependency failure returns an error response
- Important dependency failure queues for retry
- Nice-to-have dependency failure fires and forgets with logging
Common mistakes
These are the mistakes we see most often. Each one has been covered in the course, but they are worth calling out together because they are easy to fall back into.
Swallowing errors. Catching an error and doing nothing with it. The error disappears. No log, no response, no alert. The bug is invisible.
// BAD
try {
await doSomething();
} catch {} // Swallowed. No one knows it failed. Catching too early. Catching in a low-level function and returning null. The caller gets null, tries to use it, and gets a confusing TypeError instead of the original error.
No timeouts on external calls. A slow dependency holds connections, memory, and threads. Without timeouts, ten slow requests can take down the entire server.
Retrying non-idempotent operations. Retrying a POST that creates a resource creates duplicates. Only retry idempotent operations, or use idempotency keys.
Generic error messages everywhere. “Something went wrong” for every error. The user does not know what happened or what to do. Return specific messages: “Email is required,” “Product not found.”
Stack traces in responses. Exposing internal file paths, database queries, and library versions. Always return generic messages for unexpected errors. Log the details server-side.
No graceful shutdown. Docker sends SIGTERM, the server ignores it, Docker sends SIGKILL 10 seconds later. In-flight requests are dropped. Database connections are not closed.
Health check that always returns 200. The database is down but /health says “ok.” The load balancer keeps sending traffic to a broken instance.
Throwing for expected errors. Using throw and try-catch for things like “product not found” when you could just check and return a response. Throwing should be reserved for truly unexpected situations. Returning keeps the control flow simple and explicit.
Exercises
Exercise 1: Review your e-commerce API against this checklist. How many items pass?
Exercise 2: Find an error-swallowing catch block in your code (or a previous course). What should happen to the error instead?
Exercise 3: Remove the graceful shutdown handler. Send SIGTERM. Observe the difference vs with the handler.
Next up is the capstone, where we put every piece together into a complete, resilient e-commerce API.
What is the most common error handling mistake?