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.