Node.js Error Handling: From try/catch to Production Error Monitoring

Node.js Error Handling: From try/catch to Production Error Monitoring

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.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply