Has this ever happened to you? You're cruising along, enjoying the app you just built with Node and Express -- only to run head-first into an ugly error you never expected.
Or even worse, maybe you don't see anything at all! Maybe you've clicked on a link only to find your browser endlessly spinning. You take a peek at your backend logs, only to see those dreaded words:
UnhandledPromiseRejectionWarning: Unhandled promise rejection.
It's time to corral the chaos: it's time to get serious about error handling :)
Step One: Synchronous Errors
By default, when a synchronous error happens in an express route handler, Express will use its built-in error handler. It will write the error to the client, leading to those stark error pages. While Express won't expose the full stack trace in production, it's still a pretty bad experience for site visitors who bump into that obnoxious "Internal Server Error".
router.get("/bad", (req, res) => {
const a = 1;
res.send(a + b); //this will cause a ReferenceError because 'b' is not defined!
})
We can do better! Instead of leaving Express to its own devices, we can define our own custom error handler.
🛡️ Writing a custom Express error handler 🛡️
Most folks are used to writing express route handlers with 2-3 parameters: a request, a response, and optionally, a next function that can be invoked to move on to the next middleware in the chain. However, if you add a fourth parameter -- an error -- in front of the other three, this middleware becomes an error handler! When an error is thrown, the error will bypass any normal route handlers and go into the first error handler it finds downstream.
⚠️ Key point: in order to catch errors from all your routes, this error handler must be included after all route definitions! ⚠️
// First, we include all our routes:
app.use(routes);
// And at the very end, we add our custom error handler!
app.use((err, req, res, next) => {
//(Note: it's highly recommended to add in your favorite logger here :)
res.render("errorpage");
}
And voila! In one fell swoop, we made that broken route show a lovely error page:
In fact, if we wanted to get extra fancy, we could even display different error pages depending on the type of error we have received! Some folks even write their own custom Error objects (extending the native class) to store information about what status code the application should send, or which page the user should be redirected to if said Error is thrown. For purposes of this demo, however, even one 'pretty' page is already light years better than the stark error we started out with.
Step Two: Asynchronous Errors
While Express will automagically catch synchronous errors and pass them to our custom error handler, asynchronous errors are a whole different beast. If a promise is rejected without being caught in an express route handler, the unhandled rejection will prevent the client from receiving any response!
Since the 'spinning vortex of doom' is a terrible fate for our site visitors, we have to ensure we always trap promise rejections and pass them along to our error handler.
➡️ Try-Catch Blocks 🛑
By wrapping our asynchronous functions in try-catch blocks, we ensure that we are always trapping rejections when they arise. As soon as a promise is rejected, the code jumps to the 'catch' block, which then passes the error on to our handler:
const alwaysRejects = function () {
// This function creates a promise that will always reject with an error:
return new Promise((resolve, reject) => reject(new Error("I'm stuck!")));
}
router.get("/reject", async (req, res, next) => {
try {
await alwaysRejects();
res.send('Hello, World!');
} catch (err) {
next(err);
}
});
An alternative approach: Middleware for your Middleware
For an alternate method to traditional try-catch handling, The Code Barbarian recommends promisifying the route handler itself. While this option does work, it may feel a bit clunky to add a wrapper simply to avoid a try-catch.
//this function promisifies an existing express function so that any unhandled rejections within will be automagically passed to next()
function handleErr(expressFn) {
return function (req, res, next) {
expressFn(req, res, next).catch(next);
};
}
const alwaysRejects = function () {
// This function creates a promise that will always reject with an error:
return new Promise((resolve, reject) => reject(new Error("I'm stuck!")));
}
router.get("/reject", handleErr(async (req, res, next) => {
const result = await alwaysRejects();
res.send('Hello, World!');
}));
Conclusion
All in all, whichever options you pick, good error handling is here to stay!
From old-fashioned Internal Server Error to beautiful custom error pages...the glow-up is real!
Top comments (1)
Thanks For your effort i understand this topic now cuz of you , thx again ♥♥