You might be familiar with this type of errrors:
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "null".] {
code: 'ERR_UNHANDLED_REJECTION'
}
Those errors, UnhandledPromiseRejection, indicates that a promise in your JavaScript code was rejected, and this rejection was not properly handled. It can occur when there no .catch() handler attached to a promise, or when using async/await without a try-catch block to handle potential rejections.
When that happens it might be followed by:
[nodemon] app crashed - waiting for file changes before starting...
There are 2 reasons why we need to properly handle erros in express js applications:
When working with promises we need to make sure the error is caught along the code so it does not reach out in the express core code, because the default behavior is to report it and crash the server. The easiest way is to catch and handle potential errors when invoking the promise, using .then().catch() handlers or try{}catch(e){} blocks:
doSomethingAsync()
.then(result => {
// Handle the successful result
console.log(result);
})
.catch(error => {
// Handle the error
console.error('An error occurred:', error);
});
When using the async/await syntax, we should wrap your await calls in a try-catch block to catch any errors that occur during the promise execution:
async function asyncFunction() {
try {
const result = await doSomethingAsync();
console.log(result);
} catch (error) {
console.error('An error occurred:', error);
}
}
Lets' assume we have the following promise that we invoke to get a specific item:
const getItem = (key) => {
return new Promise((resolve, reject) => {
const query = `SELECT * FROM ${table} WHERE item = ?`;
this.db.get(query, [key], (err, row) => {
if (err) {
reject(err);
} else {
if (!row)
reject(err);
else{
row = mapRow(row);
resolve(row);
}
}
});
});
}
Then we invoke the function. When trying to retrieve an item which is not stored in the database if the promise is not handled, then the server will crash with an error.
router.get('/:item', async function(req, res) {
const result = await getItem(req.params.item);
res.json(result); // Send the result back to the client
});
So, the first thing to do is to handle the error in the router.
router.get('/:item', async function(req, res) {
try {
const result = await yourFunction(req.params.item);
res.json(result);
} catch (error) {
console.error(error);
// Send an HTTP 404 status code if no record is found, otherwise send a 500 for server errors
if (error.message === 'No record found') {
res.status(404).send({ message: error.message });
} else {
res.status(500).send({ message: 'Internal Server Error' });
}
}
});
If the function rejects the promise due to an error or because no record is found, the catch block will handle it. The client receives a 404 Not Found status if no record is found, indicating the specific issue, or a 500 Internal Server Error for any other errors, which is a more generic error response indicating something went wrong on the server.
router.get('/:item', function(req, res) {
getItem(req.params.item)
.then(row => { res.json(row); })
.catch(error => {
if (error.message === 'No record found') {
// in this block we can diferentiante the log level when running in prod vs dev mode
// as we don't want to show details in a real production environment
res.status(404).send({ message: error.message });
} else {
console.error(error);
res.status(500).send({ message: 'Internal Server Error' });
}
});
});
Pros
Cons
The choice between await and .then() should be guided by which makes your code more understandable and maintainable for you and your team, considering the specific context and complexity of your database operations:
async function handleDatabaseOperations() {
try {
const result1 = await dbOperation1();
const result2 = await dbOperation2(result1); // Assuming result1 is needed for result2
// More operations can be chained similarly
return finalResult; // Or handle the final result as needed
} catch (error) {
console.error(error);
throw error; // Rethrow or handle error appropriately
}
}
function handleDatabaseOperations() {
return dbOperation1()
.then(result1 => dbOperation2(result1))
// More .then() calls can be chained similarly
.then(finalResult => {
// Handle the final result
return finalResult;
})
.catch(error => {
console.error(error);
throw error; // Rethrow or handle error appropriately
});
}
When using functions that are not promisified, we need to write wrapper promises over those functions to bring it to a unitary form, without implementing any logic there. As we introduce a level of abstractization that is not necesarry in many cases, we can avoid it and use util.promisify
library. The util.promisify function converts a callback-based function (following the Node.js callback pattern) into a version that returns promises. This can make your code cleaner and avoid the explicit construction of new Promises.
First, convert the callback-based database query into a promise-returning function:
const util = require('util');
// Assuming `this.db.get` follows the Node.js callback pattern.
// Convert it into a function that returns a promise.
const dbGetAsync = util.promisify(this.db.get.bind(this.db));
// Now `dbGetAsync` can be used with async/await
Then, we use it directly in the route handler:
router.get('/:item', async function(req, res, next) {
try {
const query = `SELECT * FROM ${table} WHERE slug = ?`;
const row = await dbGetAsync(query, [req.params.item]);
if (!row) {
const error = new Error('No record found');
error.status = 404; // Add an HTTP status code to the error
throw error; // Pass the error to the catch block
}
const result = mapRow(row);
res.json(result);
} catch (error) {
next(error); // Pass the error to the next error handling middleware
}
});
Using util.promisify has certain advantages, but in the same time it makes the code more tightly coupled. If in the future we need to replace the database with another connector then writng promises as an extra layer would be more usefull.
If you're working extensively with promises, consider using libraries like Bluebird, which offer additional features for error handling and can simplify some aspects of catching errors that might otherwise be unhandled.
In Express, we can define an error-handling middleware that catches any errors passed to the next function. This allows us to handle all errors in one place, making the route handlers cleaner and more focused on their primary responsibilities.
We can add an error-handling middleware at the end of your middleware stack:
app.use(function(err, req, res, next) {
console.error(err.stack);
const statusCode = err.status || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).send({ message });
});
This setup allows us to handle errors more cleanly in route handlers by using next(error) when an error occurs. The error-handling middleware will then catch and process this error, sending an appropriate response to the client.
Combining this with util.promisify simplifies working with asynchronous code by avoiding the manual wrapping of promises, and centralizing error handling through middleware reduces repetitive error-handling code in your route handlers, making applications more maintainable and scalable.
Unhandled promise rejections and uncaught exceptions are two distinct types of errors in Node.js and JavaScript applications that can lead to different kinds of issues, including crashing your application. Understanding and correctly handling both is crucial for maintaining the stability and reliability of your applications. Let's clarify these concepts and how to handle them:
An Unhandled Promise Rejection occurs when a promise is rejected (i.e., it results in an error), and no .catch() handler is attached to it, nor is it caught by an async/await try-catch block. This situation might lead to potential memory leaks, unexpected application behavior, or crash in future versions of Node.js. Starting with Node.js version 15.0.0, unhandled promise rejections terminate the Node.js process by default, although this behavior can be customized.
Using .catch() with Promises: Always attach a .catch() handler to your promises. Using try-catch with Async/Await: When using async/await, wrap your await calls in a try-catch block to handle any errors. Global Handler: You can listen for unhandled promise rejections globally using the process.on('unhandledRejection', handler) event. This should be used sparingly, mainly for logging and debugging, not as a primary error-handling strategy.
Use the process.on('unhandledRejection') event listener to catch unhandled promise rejections, which can otherwise lead to application crashes.
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Handle the error here (e.g., log it, send email notification, etc.)
});
Middleware in Express does not directly prevent UnhandledPromiseRejection from crashing the server, as these rejections may occur outside the request-response cycle. However, if an async function within a route handler or middleware doesn't catch a rejection (e.g., missing await or .catch()), it can lead to unhandled promise rejections. Ensuring that all asynchronous operations in your middleware and route handlers properly handle rejections will mitigate this risk.
In summary, while your error handling middleware is crucial for managing errors that occur during request processing, global handlers for unhandledRejection and uncaughtException are necessary to manage the broader class of errors and promise rejections that can occur in a Node.js application.
Similarly, catch uncaught exceptions with process.on('uncaughtException'). However, after logging the error or performing cleanup, you should consider restarting the application as it might be in an undefined state.
An Uncaught Exception occurs when an error is thrown synchronously in the application and is not caught by any try-catch blocks. Uncaught exceptions can lead to the immediate termination of the Node.js process, potentially causing your application to crash.
How to Handle:
Try-Catch: Use try-catch blocks around code that might throw exceptions. Global Handler: Node.js provides a way to catch all uncaught exceptions using process.on('uncaughtException', handler). This allows you to log the error, perform cleanup, and potentially keep the process running. However, be cautious; after an uncaught exception, your application might be in an undefined state, and it is often recommended to restart the process after logging the error and performing any necessary cleanup.
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Perform cleanup if necessary
process.exit(1); // Exiting is recommended after handling the exception
});
An uncaught exception can potentially crash an Express server by causing the Node.js process to exit. This is because an uncaught exception may leave your application in an unpredictable state, making it unsafe to continue running.
Middleware functions in Express are scoped to handle errors that occur in the request-response cycle and are passed along via the next(err) function. Since uncaught exceptions are, by definition, not caught by the application code, they do not propagate through the Express middleware stack. Therefore, while middleware is great for handling operational errors that you anticipate might occur during normal operations (like database errors, missing files, etc.), it is not suitable for catching programming errors that lead to uncaught exceptions.
Origin: Unhandled promise rejections originate from promises that fail without a .catch() handler, while uncaught exceptions originate from synchronous code that throws an error outside of a try-catch block. Handling: Unhandled promise rejections are specifically tied to the Promise API and async/await syntax and are handled using .catch() or try-catch. In contrast, uncaught exceptions are caught using try-catch blocks or the uncaughtException event for errors thrown synchronously. Impact: Both can lead to application instability or crashes, but they come from different sources and require different handling strategies. Properly handling both unhandled promise rejections and uncaught exceptions is essential for creating resilient Node.js applications. It's also a good practice to use monitoring and alerting tools to detect these issues in production environments.
Note: Exiting the process on uncaughtException is often recommended because the application may be in an unknown state. Consider using a process manager like PM2, Forever, or Node.js cluster to restart the app automatically.
Relying on a process manager like PM2 to restart your server when it crashes is a common practice in production environments for ensuring high availability. However, it's important to balance this approach with proper error handling within your application. Let's explore the considerations:
The Importance of Proper Error Handling
Best Practices
In summary, while PM2 or similar process managers are valuable tools for maintaining uptime, they should complement, not replace, proper error handling within your application. The goal should be to create a robust and resilient application where unhandled errors are exceptions, not the norm, and PM2 serves as a safety net for unforeseen issues rather than a primary error recovery mechanism.
Mastering error handling in ExpressJS: Prevent crashes and enhance stability with effective strategies for managing unhandled rejections and exceptions.
Mastering error handling in ExpressJS: Prevent crashes and enhance stability with effective strategies for managing unhandled rejections and exceptions.
Mastering error handling in ExpressJS: Prevent crashes and enhance stability with effective strategies for managing unhandled rejections and exceptions.
Mastering error handling in ExpressJS: Prevent crashes and enhance stability with effective strategies for managing unhandled rejections and exceptions.