Errors are an inevitable part of software development. A database might be offline, a user might upload an invalid file, or you might simply have a typo in your logic.
In Node.js, unhandled errors can crash your entire application, knocking your server offline for all users. Therefore, mastering Error Handling is an essential skill for writing robust, production-ready Node.js backends.
For synchronous code (code that runs line-by-line without waiting for I/O operations), the standard JavaScript try...catch block is the best approach.
If any code inside the try block throws an error, execution stops immediately and jumps straight into the catch block.
try {
console.log("Attempting to parse user data...");
// Simulating broken JSON from an API
const badJson = '{"name": "John", "age" : 30'; // Missing closing brace
const user = JSON.parse(badJson); // This line throws an Error
console.log("This line will never execute.");
} catch (error) {
console.error("Oops! An error occurred.");
console.error("Error Message:", error.message);
}
Handling errors in asynchronous code depends heavily on the pattern you are using: Callbacks, Promises, or Async/Await.
As we discussed in earlier tutorials, callbacks in Node.js follow the Error-First convention. You must manually check if the err object exists.
const fs = require('fs');
fs.readFile('non-existent-file.txt', (err, data) => {
if (err) {
// Handle the error here
console.error("File Read Error:", err.code);
return; // Exit the function to prevent further execution
}
console.log(data);
});
When using Promises, errors (or "rejections") are handled by attaching a .catch() method at the end of your promise chain.
fetchDatabaseRecords()
.then(records => processRecords(records))
.catch(error => {
console.error("Promise Rejected:", error.message);
});
Because async/await allows you to write asynchronous code synchronously, you can wrap your await calls in a standard try...catch block!
async function getUser() {
try {
const user = await database.findUser({ id: 99 });
console.log(user.name);
} catch (error) {
console.error("Failed to fetch user:", error.message);
}
}
Sometimes, you want to trigger (or "throw") your own errors when something goes wrong with business logic. You can do this using the throw new Error() syntax.
function checkAge(age) {
if (age < 18) {
// Throwing an error intentionally
throw new Error("User must be 18 or older to access this site.");
}
return "Access Granted!";
}
try {
console.log(checkAge(16));
} catch (error) {
console.error("Access Denied:", error.message);
}
What happens if an error slips through your try...catch blocks? By default, Node.js will log the error and crash the process.
You can implement global listeners as a last line of defense to elegantly log the error and safely restart your server, rather than crashing silently.
// Catches unhandled synchronous errors
process.on('uncaughtException', (err) => {
console.error('CRITICAL: Uncaught Exception:', err);
// Usually, you should exit the process and let a tool like PM2 restart it
process.exit(1);
});
// Catches unhandled Promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('CRITICAL: Unhandled Rejection at:', promise, 'reason:', reason);
});
Which syntax is used to handle errors when using async/await?