Key Takeaways
Error handling is a critical aspect of building robust Node.js applications that can withstand real-world conditions. When applications lack proper error handling, they can crash unexpectedly, behave unpredictably, or present users with cryptic messages that damage trust and usability. As applications grow in complexity, solid error handling becomes even more important.
Many developers overlook error management, treating it as an afterthought rather than a core part of application architecture. This guide will provide practical, implementable techniques for handling errors in Node.js applications, with real code examples that you can adapt to your projects. We’ll explore synchronous and asynchronous error handling, examine different error types, and discuss best practices for logging and monitoring.
Set up basic Node.js error handling
Use try-catch with sync code
The fundamental building block of error handling in JavaScript is the try-catch statement. For synchronous code, this approach works reliably to prevent application crashes.
Here’s a simple example:
function divideNumbers(a, b) {
try {
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
} catch (error) {
console.error('An error occurred:', error.message);
return null; // Return a fallback value
}
}
// Usage
const result = divideNumbers(10, 0);
console.log(result); // null (instead of crashing)
This pattern is effective because it:
- Contains the error locally rather than allowing it to crash the program
- Provides clear feedback about what went wrong
- Allows the function to return a fallback value
Check for node js error types
Node.js has several built-in error types that help identify specific issues. Understanding these error classes helps you handle them appropriately:
- Error: The base error class from which all others inherit
- SyntaxError: Occurs when JavaScript code can’t be parsed
- ReferenceError: Thrown when an undefined variable is referenced
- TypeError: Indicates an operation on an incorrect type
- RangeError: Occurs when a value is not within the expected range
- URIError: Thrown when URI handling functions receive invalid inputs
- SystemError: Represents errors raised by the Node.js runtime
You can check error types using the instanceof operator:
try {
nonExistentFunction();
} catch (error) {
if (error instanceof ReferenceError) {
console.log('This is a reference error');
} else if (error instanceof TypeError) {
console.log('This is a type error');
} else {
console.log('This is another type of error');
}
}
Handle thrown objects carefully
In JavaScript, you can throw any value, not just Error objects. However, for clarity and consistency, it’s best to throw Error instances or objects that inherit from Error.
Create custom error classes to differentiate between error types in your application:
Staff Augmentation Service
Tap Into a Talent Ecosystem Powered by 1500+ Agencies and 1,900+ Projects. EMB’s Staff Augmentation Services Help You Stay Agile, Competitive, and Fully Resourced.
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
this.date = new Date();
}
}
try {
throw new DatabaseError('Connection failed', 'SELECT * FROM users');
} catch (error) {
if (error instanceof DatabaseError) {
console.error(`${error.name}: ${error.message}`);
console.error(`Failed query: ${error.query}`);
console.error(`Time: ${error.date}`);
} else {
console.error('An unexpected error occurred:', error);
}
}
Handle async errors in Node.js
Use try-catch in async functions
Async/await makes error handling cleaner for asynchronous operations. You can use try-catch blocks just like with synchronous code:
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch user data:', error.message);
// You might want to rethrow or return a default value
return { error: true, message: error.message };
}
}
// Usage
async function displayUser() {
const userData = await fetchUserData(123);
if (userData.error) {
// Handle the error case
showErrorMessage(userData.message);
} else {
// Process the data
updateUserInterface(userData);
}
}
Return promises with .catch handlers
When working with promises directly, always attach .catch() handlers to handle rejections:
function readFile(path) {
return fs.promises.readFile(path, 'utf8')
.then(data => {
return JSON.parse(data);
})
.catch(error => {
if (error instanceof SyntaxError) {
console.error('Invalid JSON format:', error.message);
return {}; // Return empty object as fallback
}
// For file system errors, rethrow
console.error('File system error:', error.message);
throw error; // Rethrow to let caller handle it
});
}
// Usage
readFile('config.json')
.then(config => {
// Use the config
})
.catch(error => {
// Handle any rethrown errors
console.error('Could not load configuration:', error.message);
});
Listen to unhandledRejection events
For promises that reject without a .catch handler, Node.js emits an ‘unhandledRejection’ event. Listening for this event provides a safety net for your application:
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Promise Rejection:');
console.error('- Reason:', reason);
// Optional: Log the promise that caused the rejection
console.error('- Promise:', promise);
// You might want to terminate the process in production
// process.exit(1);
});
// This will trigger the unhandledRejection handler
Promise.reject(new Error('Something went wrong'));
Similarly, you should listen for uncaught exceptions:
process.on('uncaughtException', error => {
console.error('Uncaught Exception:');
console.error(error);
// Perform cleanup if needed
// It's best practice to exit after an uncaught exception
process.exit(1);
});
Log, group, and trace errors smartly
Follow node js error logging best practices
Effective error logging should provide enough context to understand and fix issues without overwhelming your logs:
- Include stack traces for developer debugging
- Add context information (user ID, request ID, etc.)
- Use structured logging (JSON) for machine readability
- Set appropriate log levels (error, warn, info, debug)
- Avoid logging sensitive information
function logError(error, context = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
error: {
message: error.message,
name: error.name,
stack: error.stack
},
context: {
...context,
environment: process.env.NODE_ENV
}
};
// For production, use a proper logging library
console.error(JSON.stringify(logEntry));
}
// Usage
try {
// Some operation
} catch (error) {
logError(error, {
userId: 'user-123',
action: 'payment-processing'
});
}
Include timestamps and error codes
Adding timestamps and custom error codes makes errors more traceable and helps with pattern recognition:
// Define error codes in a central location
const ERROR_CODES = {
DATABASE: {
CONNECTION: 'DB001',
QUERY: 'DB002',
TRANSACTION: 'DB003'
},
API: {
REQUEST: 'API001',
RESPONSE: 'API002',
VALIDATION: 'API003'
}
};
class AppError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = 'AppError';
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
}
toJSON() {
return {
error: this.name,
code: this.code,
message: this.message,
details: this.details,
timestamp: this.timestamp
};
}
}
// Usage
throw new AppError(
'Failed to update user profile',
ERROR_CODES.DATABASE.QUERY,
{ userId: 'user-123', fields: ['name', 'email'] }
);
Integrate centralized logging tools
For production applications, use specialized logging libraries and services:
// Example using Winston
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
// Write logs to console
new winston.transports.Console(),
// Write errors to error.log
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
// Write all logs to combined.log
new winston.transports.File({
filename: 'combined.log'
})
]
});
// Usage
try {
// Application code
} catch (error) {
logger.error('Operation failed', {
error: {
message: error.message,
stack: error.stack
},
context: {
userId: 'user-123',
requestId: 'req-456'
}
});
}
Consider using services like Sentry, LogRocket, or Datadog for centralized error tracking in distributed systems.
Master error handling by combining multiple techniques
Effective Node.js error handling combines several layers of protection. Start with proper try-catch blocks at the function level, use specific error types for clear categorization, implement global error handlers as a safety net, and create a consistent logging strategy.
Remember that error handling isn’t just about preventing crashes. It’s about creating applications that degrade gracefully, provide useful feedback, and allow for quick problem resolution. By implementing the techniques covered in this guide, you’ll build more reliable Node.js applications that can stand up to real-world usage conditions.
The most resilient applications use a defense-in-depth approach to error handling. No single technique catches all problems, but together they create a robust system that can withstand unexpected conditions while maintaining data integrity and user experience.
FAQ
Q1. What are the main node js error types I should handle?
The main Node.js error types to handle include Error (base class), SyntaxError, ReferenceError, TypeError, RangeError, URIError, and SystemError. Additionally, many Node.js modules throw specific error types like FSError for file system operations. Always check for these specific types to provide tailored error handling based on the error nature.
Q2. What are effective error logging tools for Node.js apps?
Winston and Pino are popular Node.js logging libraries that support structured logging with multiple transport options. For error monitoring in production, tools like Sentry, LogRocket, New Relic, and Datadog provide real-time alerts, error grouping, and performance monitoring. Choose based on your specific needs for verbosity, performance, and integration capabilities.
Q3. Is it safe to use a global error handler in Node.js?
Global error handlers like process.on(‘uncaughtException’) should be used carefully. They’re useful as a last resort to log errors and perform cleanup before graceful shutdown, but shouldn’t replace proper error handling at the function level. In production, after logging the error, it’s generally recommended to restart the process since the application state may be corrupted.
Q4. How does Express.js simplify Node.js error handling?
Express.js provides middleware-based error handling through special four-parameter functions (err, req, res, next). This allows you to centralize error handling logic while still customizing responses based on error types. Express also supports async error handlers with the express-async-errors package, eliminating the need for try-catch blocks in route handlers when using async functions.
