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.
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.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
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.
