How to handle errors in Node.js?
Robust error handling is crucial for building stable and resilient Node.js applications. Node.js offers several mechanisms to deal with different types of errors, including synchronous, asynchronous, and system errors. Understanding these methods is key to creating reliable services.
Synchronous Errors: try...catch
For synchronous code, the standard JavaScript try...catch block is used to catch exceptions that occur within the try block. This mechanism prevents the application from crashing due to unexpected synchronous errors.
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero is not allowed.');
}
return a / b;
}
try {
const result = divide(10, 2);
console.log('Result:', result);
const errorResult = divide(10, 0);
console.log('Error Result (should not reach here):', errorResult);
} catch (error) {
console.error('Synchronous error caught:', error.message);
}
Asynchronous Errors: Callbacks (Error-First Pattern)
In older Node.js code and with many core modules, the error-first callback pattern is common. The first argument to a callback function is reserved for an Error object, or null if no error occurred. It's crucial to check for this error argument first.
const fs = require('fs');
fs.readFile('nonexistent.txt', 'utf8', (err, data) => {
if (err) {
console.error('File read error:', err.message);
// Handle the error, perhaps by notifying the user or logging it
return;
}
console.log('File content:', data.substring(0, 50) + '...');
});
fs.readFile('package.json', 'utf8', (err, data) => {
if (err) {
console.error('File read error:', err.message);
return;
}
console.log('package.json content loaded successfully.');
});
Asynchronous Errors: Promises
Promises provide a more structured way to handle asynchronous operations. Errors in a promise chain are caught using the .catch() method, or the second argument to .then(). The .catch() method is generally preferred for readability.
function fetchData() {
return new Promise((resolve, reject) => {
// Simulate an async operation that might fail
setTimeout(() => {
const success = Math.random() > 0.5; // Simulate success or failure randomly
if (success) {
resolve('Data fetched successfully!');
} else {
reject(new Error('Failed to fetch data. Network issues?'));
}
}, 200);
});
}
fetchData()
.then(data => console.log('Promise success:', data))
.catch(error => console.error('Promise error caught:', error.message));
// Chaining promises and error handling
Promise.resolve(1)
.then(result => {
console.log('Step 1:', result);
if (result === 1) {
throw new Error('Intentional error in chain');
}
return result + 1;
})
.then(result => console.log('Step 2 (should not reach here):', result))
.catch(err => console.error('Promise chain error:', err.message));
Asynchronous Errors: Async/Await
async/await syntax allows writing asynchronous code that looks and behaves more like synchronous code. This makes error handling with try...catch blocks intuitive for promises, as rejections are effectively 'thrown' and caught.
async function fetchDataAsync() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = false; // Force failure for demonstration
if (success) {
resolve('Data fetched successfully with async/await!');
} else {
reject(new Error('Failed to fetch data via async/await.'));
}
}, 200);
});
}
async function processData() {
try {
console.log('Attempting to fetch data with async/await...');
const data = await fetchDataAsync();
console.log('Async/Await success:', data);
} catch (error) {
console.error('Async/Await error caught:', error.message);
}
}
processData();
Event Emitters
Objects that inherit from EventEmitter (like streams, HTTP servers, etc.) emit an error event when an internal error occurs. If no listener is registered for the error event, Node.js will throw an uncaught exception, which typically crashes the process. It's vital to always add an error listener for EventEmitter instances.
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// Register an error listener to prevent process crash
myEmitter.on('error', (err) => {
console.error('Emitter error caught:', err.message);
// Depending on the severity, you might want to log more details
// or perform specific cleanup.
});
// Simulate an error being emitted
myEmitter.emit('error', new Error('Something went wrong within the emitter!'));
// Example of an event emitter that doesn't emit errors frequently
class MyResource extends EventEmitter {
constructor() {
super();
setInterval(() => {
if (Math.random() > 0.8) {
this.emit('error', new Error('Resource failure!'));
}
this.emit('data', 'some data');
}, 500);
}
}
const resource = new MyResource();
resource.on('data', (data) => console.log('Resource data:', data));
resource.on('error', (err) => console.error('MyResource error:', err.message));
Global Uncaught Exceptions and Unhandled Rejections
Node.js provides global handlers for errors that escape all other mechanisms. process.on('uncaughtException') catches synchronous errors not caught by try...catch. process.on('unhandledRejection') catches Promise rejections not handled by a .catch() block. These should generally be used for logging, cleanup, and graceful shutdown (i.e., exiting the process), *not* for resuming application state, as the application might be in an unpredictable state after such an error.
// Always add these handlers at the very top of your application entry point
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception caught:', err.message, err.stack);
// Log the error, perform urgent cleanup, and then terminate the process.
// It's unsafe to continue normal operation after an uncaught exception.
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason.message, reason.stack);
// Log the error. For unhandled rejections, you might decide to terminate
// or just log, depending on strictness and whether the rejection indicates
// a critical unrecoverable state. Often, it's safer to exit.
process.exit(1);
});
// --- Examples (uncomment to test, but be aware they will terminate the process) ---
// Example of uncaught synchronous exception
// setTimeout(() => {
// console.log('Throwing uncaught synchronous error...');
// throw new Error('This is an uncaught synchronous error!');
// }, 500);
// Example of unhandled promise rejection
// setTimeout(() => {
// console.log('Rejecting a promise without a .catch() handler...');
// Promise.reject(new Error('This is an unhandled promise rejection!'));
// }, 1000);
Custom Error Classes
Creating custom error classes that extend the built-in Error class can significantly improve error identification, categorisation, and handling logic, especially for domain-specific errors in larger applications. This allows you to attach additional properties (like HTTP status codes or custom error codes) and use instanceof for type-checking.
class ValidationError extends Error {
constructor(message, fields = []) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
this.fields = fields;
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
function validateInput(data) {
if (!data || !data.username) {
throw new ValidationError('Username is required.', ['username']);
}
if (data.password.length < 6) {
throw new ValidationError('Password must be at least 6 characters.', ['password']);
}
return true;
}
try {
validateInput({ username: 'user123', password: 'abc' });
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation Error (${error.statusCode}): ${error.message} Fields: ${error.fields.join(', ')}`);
} else {
console.error('An unexpected error occurred:', error.message);
}
}
try {
throw new NotFoundError('User with ID 123 not found.');
} catch (error) {
if (error instanceof NotFoundError) {
console.error(`Not Found Error (${error.statusCode}): ${error.message}`);
} else {
console.error('An unexpected error occurred:', error.message);
}
}
Best Practices
- Always Handle Errors: Never let an error go unhandled, as it can crash your application or lead to unpredictable behavior.
- Log Errors Effectively: Include stack traces, timestamps, and relevant context in your logs. Use dedicated logging libraries (e.g., Winston, Pino).
- Differentiate Sync vs. Async: Use
try...catchfor synchronous code andPromise.catch(),async/await try...catch, or error-first callbacks for asynchronous operations. - Avoid Global Handlers for Recovery:
process.on('uncaughtException')andprocess.on('unhandledRejection')should be used for graceful shutdown and critical error logging, not for attempting to resume normal application flow. - Throw
ErrorObjects: Always throw instances ofErroror custom error classes, not strings or other primitives, to preserve stack traces and other useful information. - Propagate Errors: If a component cannot fully handle an error, re-throw or reject it to allow higher-level components to decide on the appropriate action.
- Use Custom Errors: Define custom error classes for domain-specific errors to improve clarity, type-checking, and targeted error handling.
- Centralized Error Handling: In web applications (e.g., Express), use middleware for a single point of error handling to catch and format errors consistently.