intermediate

Error handling

Separate operational errors from programmer bugs, map errors to responses, preserve causes, and avoid unhandled rejections.

Distinguish operational errors (expected failures — validation, 404, upstream timeout) from programmer errors (bugs — null dereference, invariant violation). Operational errors get mapped to safe HTTP responses; programmer errors may crash after logging so a supervisor restarts a corrupted process.

					class AppError extends Error {
  constructor(message, { status = 500, code = 'INTERNAL', cause }) {
    super(message, { cause });
    this.status = status;
    this.code = code;
    this.isOperational = true;
  }
}

process.on('unhandledRejection', (reason) => {
  logger.error({ reason }, 'unhandled rejection');
  shutdown(1);
});
				

Always forward async errors to Express via `next(err)` or framework equivalent. Preserve `cause` chains for debugging; strip internal details from client JSON.

On interviews: operational vs programmer taxonomy; centralized error middleware shape; why swallowing promise rejections is dangerous.

Common pitfalls: empty `catch {}` blocks; returning 500 for validation with stack traces; different error shapes per route.

The trade-off is balancing simplicity, performance, safety, and operability — name which axis you optimized and what cost you accepted.

Checklist:

  • Typed/structured application errors.
  • One response mapper for HTTP API.
  • Monitor unhandledRejection and uncaughtException.
  • Log stack server-side only.