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

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

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.

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

Related: Node.js Error Handling: From try/catch to Production Error Monitoring · Express.js Best Practices: Build Production-Ready APIs With Node.js · Node.js Security Best Practices: Protect Your API From the Most Common Attacks

Related: Lambda Timeouts, Retries, and DLQs: Idempotent Failure Handling · Node.js Async Hooks: The Tracing API That Powers Every APM Tool · Python Context Managers: Master the with Statement for Clean Resource Handling

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


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 comment

No account needed. Name and email are optional.