An error the user can't understand, and a failure you can't diagnose, are both bugs — treat error handling as a first-class part of every feature, not cleanup at the end.
Fail loudly in development, gracefully in production
- Never silently swallow an exception (an empty
catch block, a caught error that's just logged and ignored) — if you catch it, either handle it meaningfully or re-throw it. A swallowed error is a bug that will resurface later with no trail.
- In production, never expose stack traces, internal error messages, or raw exception text to end users — show a friendly, generic message, and log the real detail server-side.
Distinguish error types
- Separate expected, recoverable conditions (validation failure, "not found," a declined payment) from unexpected, unrecoverable ones (a database connection loss, a null you never expected). Handle the former with normal control flow and specific user-facing messaging; let the latter propagate to a top-level handler that logs and alerts.
- Use typed/structured errors (an error code or class, not just a string message) wherever the caller needs to branch on what went wrong — string-matching an error message to decide behavior breaks the moment the message wording changes.
User-facing messages
- Every user-facing error message should say what happened and, where possible, what the user can do next ("Your session expired — please sign in again" beats "Error 401").
- Never show a blank screen or a silent failure on an unhandled error — always render a fallback UI (error boundary, friendly error page) so the user knows something went wrong rather than assuming the app is just broken or slow.
Retries and timeouts
- Any network or external-service call needs an explicit timeout — an unbounded request can hang a request thread or a user's UI indefinitely.
- Retry only idempotent operations, and always with backoff (exponential, with jitter) — a tight retry loop against a struggling downstream service makes the outage worse, not better.
- Cap total retry attempts and surface a clear failure to the caller once the cap is hit; an infinite retry loop is indistinguishable from a hang to the end user.
Logging
- Log enough context to diagnose the failure without a follow-up question: what operation, what input (redacted of secrets/PII), what the underlying error was, and a correlation/request ID that ties it to the specific request.
- Never log secrets, tokens, passwords, or full payment/PII details, even at debug level — treat logs as a place attackers and auditors both eventually read.
Boundaries
- Wrap every route handler / API endpoint / background job entrypoint in a top-level error boundary that catches anything unhandled, logs it, and returns a safe, well-formed response — an unhandled exception should never produce a raw 500 with no logging or a hung process.
- Validate external input (API responses, file contents, third-party webhooks) defensively — never assume a well-formed shape just because it's "supposed to" be well-formed.
Comments (0)
Sign in to comment. Sign in
No comments yet. Be the first to add one.