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 log and swallow, promises that reject silently, and generic “Internal Server Error” responses that tell users nothing and operators less. Production-quality error handling requires custom error classes, structured logging, global handlers for uncaught exceptions, and integration with error tracking services.

TL;DR: Custom error classes with code, statusCode, and isOperational properties. Express error middleware (4 params) at the end. Use asyncHandler wrapper to catch async route errors. Handle unhandledRejection and uncaughtException globally. Integrate Sentry or similar for production alerting.

Custom error classes — structured errors

// Base error class
class AppError extends Error {
  constructor(message, code, statusCode = 500, isOperational = true) {
    super(message);
    this.name = this.constructor.name;
    this.code = code;          // Machine-readable: 'USER_NOT_FOUND'
    this.statusCode = statusCode; // HTTP status
    this.isOperational = isOperational; // false = programmer error
    Error.captureStackTrace(this, this.constructor);
  }
}

// Domain-specific errors
class NotFoundError extends AppError {
  constructor(resource, id) {
    super(`${resource} ${id} not found`, 'NOT_FOUND', 404);
  }
}

class ValidationError extends AppError {
  constructor(message, fields = {}) {
    super(message, 'VALIDATION_FAILED', 422);
    this.fields = fields; // Field-level errors
  }
}

class UnauthorizedError extends AppError {
  constructor(msg = 'Authentication required') {
    super(msg, 'UNAUTHORIZED', 401);
  }
}

// Usage:
throw new NotFoundError('User', userId);
throw new ValidationError('Invalid input', { email: 'Must be valid email' });

Async error handling — the complete pattern

// asyncHandler: wraps async route handlers to catch rejections
const asyncHandler = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Route with proper error handling:
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);
}));
// No try/catch in route — asyncHandler passes rejection to next()

// Express error middleware — MUST have 4 params
app.use((err, req, res, next) => {
  const error = {
    code: err.code || 'INTERNAL_ERROR',
    message: err.isOperational ? err.message : 'An unexpected error occurred',
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
  };

  // Log all errors with context
  logger.error('Request error', {
    error: { code: err.code, message: err.message, stack: err.stack },
    request: { method: req.method, url: req.url, userId: req.user?.id },
    statusCode: err.statusCode || 500
  });

  // Report non-operational errors to Sentry
  if (!err.isOperational) {
    Sentry.captureException(err);
  }

  res.status(err.statusCode || 500).json({ error });
});

Global handlers — catch everything

// Handle unhandled Promise rejections
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Promise Rejection', { reason, promise });
  Sentry.captureException(reason);
  // Give time to log, then exit (let process manager restart)
  setTimeout(() => process.exit(1), 1000);
});

// Handle uncaught synchronous exceptions
process.on('uncaughtException', (error) => {
  logger.error('Uncaught Exception — shutting down', { error });
  Sentry.captureException(error);
  setTimeout(() => process.exit(1), 1000);
});

// Graceful shutdown on SIGTERM (Kubernetes sends this)
process.on('SIGTERM', async () => {
  logger.info('SIGTERM received — starting graceful shutdown');
  server.close(async () => {
    await db.end(); // Close DB connections
    await cache.quit(); // Close Redis
    logger.info('Graceful shutdown complete');
    process.exit(0);
  });
  // Force exit after 30s if graceful fails
  setTimeout(() => process.exit(1), 30000);
});

Structured error logging with Pino

const pino = require('pino');
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: label => ({ level: label }), // Use string level not number
  },
  base: { service: 'api', version: process.env.npm_package_version },
});

// Every error log includes context:
logger.error({
  err: { code: error.code, message: error.message, stack: error.stack },
  req: { method: 'POST', url: '/payments', userId: '123', traceId: req.id },
  duration_ms: Date.now() - req.startTime,
}, 'Payment processing failed');
// Output: {"level":"error","service":"api","req":{...},"err":{...},"msg":"..."}
  • ✅ Custom error classes with code, statusCode, isOperational
  • ✅ asyncHandler wrapper — no try/catch in every route
  • ✅ 4-param Express error middleware at end of app
  • ✅ unhandledRejection and uncaughtException handlers with exit
  • ✅ Structured JSON logs with request context on every error
  • ✅ Sentry/Datadog for non-operational errors
  • ❌ Never swallow errors in empty catch blocks
  • ❌ Never exit process without logging the error first

Node.js error handling connects to API security — never expose stack traces or internal details in error responses. The CI/CD guide covers how error monitoring integrates into deployment pipelines. External reference: Joyent Node.js error handling 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