Most Node.js error handling is an afterthought — catch blocks that swallow errors and generic 500 responses. Production-quality error handling requires custom error classes, structured logging, global handlers, and integration with error tracking services like Sentry.
⚡ TL;DR: Custom AppError with code/statusCode/isOperational. asyncHandler wrapper for async routes. 4-param Express error middleware at end. unhandledRejection handler that exits. Structured JSON logging. Sentry for non-operational errors.
Custom error hierarchy
class AppError extends Error {
constructor(message, code, statusCode=500, isOperational=true) {
super(message);
this.code = code; // 'USER_NOT_FOUND'
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} ${id} not found`, 'NOT_FOUND', 404);
}
}
class ValidationError extends AppError {
constructor(msg, fields={}) {
super(msg, 'VALIDATION_FAILED', 422);
this.fields = fields;
}
}
asyncHandler — no try/catch in routes
const asyncHandler = fn => (req,res,next) =>
Promise.resolve(fn(req,res,next)).catch(next);
app.get('/users/:id', asyncHandler(async (req,res) => {
const user = await UserService.findById(req.params.id);
if(!user) throw new NotFoundError('User', req.params.id);
res.json(user);
}));
Express error middleware
// MUST have 4 params — tells Express it's error handler
app.use((err, req, res, next) => {
logger.error('Request error', {
err: {code:err.code, message:err.message},
req: {method:req.method, url:req.url, userId:req.user?.id}
});
if(!err.isOperational) Sentry.captureException(err);
res.status(err.statusCode||500).json({
error: {
code: err.code||'INTERNAL_ERROR',
message: err.isOperational ? err.message : 'Something went wrong'
}
});
});
Global handlers
process.on('unhandledRejection',(reason)=>{
logger.error('Unhandled rejection',{reason});
Sentry.captureException(reason);
setTimeout(()=>process.exit(1),1000);
});
process.on('uncaughtException',(error)=>{
logger.error('Uncaught exception',{error});
Sentry.captureException(error);
setTimeout(()=>process.exit(1),1000);
});
process.on('SIGTERM',async()=>{
server.close(async()=>{ await db.end(); process.exit(0); });
setTimeout(()=>process.exit(1),30000);
});
- ✅ Custom error classes with code, statusCode, isOperational
- ✅ asyncHandler — single wrapper instead of try/catch in every route
- ✅ 4-param error middleware at end of Express app
- ✅ Global unhandledRejection + uncaughtException handlers
- ✅ Graceful shutdown on SIGTERM
- ❌ Never swallow errors in empty catch blocks
- ❌ Never expose stack traces in production responses
Error handling pairs with API security — never expose internals. External reference: Joyent Node.js error guide.
Recommended Reading
→ Designing Data-Intensive Applications — The bible of distributed systems and production engineering at scale.
→ The Pragmatic Programmer — Timeless engineering wisdom every senior developer needs.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Join 2,000+ Senior Developers
Get expert-level JavaScript, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
