Mastering Express.js Error Handling: From Custom Errors to Enhanced Error Responses
Victoria Johnston
Posted on August 18, 2023
Introduction
Hello again, it’s Victoria here 👋
Let’s be real. In software development, errors are a given! It’s not about avoiding errors, but rather how you manage them that truly separates a well-constructed piece of software from the rest.
Today, we’re going to delve into the world of error handling within Express.js, a versatile and minimalist web application framework for Node.js. We’ll go from understanding how Express.js processes errors, to creating and applying custom errors, all while enhancing the quality of your code!
I. Understanding Error Handling in Express.js
Express.js comes equipped with built-in error handling mechanisms. However, to leverage these effectively, it’s crucial to comprehend how errors propagate within Express.
Consider a classic Express.js middleware:
app.get('/users', async (req, res, next) => {
try {
const users = awaitgetUsersFromDatabase();
res.json(users);
} catch (error) {
next(error);
}
});
In this scenario, if getUsersFromDatabase() throws an error, the catch block captures it and passes it to next(). This handoff facilitates the error's journey down the middleware stack until it encounters our dedicated error handling middleware. By delegating error handling to specific middleware, we ensure consistency across our routes and sidestep code duplication. This insightful article from the Express.js guide offers a deeper look.
II. Custom Error Handling Middleware in Express.js
Error handling middleware in Express.js resembles other middleware, save for one critical difference: it accepts four arguments ((err, req, res, next)) instead of three. Here's a rudimentary example:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send('An error occurred!');
});
In this instance, all errors, irrespective of their type or severity, receive the same bland response. To make this more dynamic, we need to generate and employ custom errors.
This basic error handling middleware does its job, but it lacks usability! Each error triggers the same monotonous response, “An error occurred!” Along with the error status code, this message makes it challenging to identify the root cause of the issue. We might see the original error logged, but without context, debugging can be cumbersome. To inject expressiveness and utility into our errors, let’s craft some custom ones.
III. Creating Custom Errors
Constructing custom errors not only imbues our errors with greater context but also streamlines the process of identifying and resolving them.
Let’s fashion a DatabaseError and a ValidationError class:
class DatabaseError extends Error {
constructor(message) {
super(message);
this.name = 'DatabaseError';
this.statusCode = 500;
}
}
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
}
}
These classes augment the native Error class and incorporate a statusCode property. We'll harness this property when formulating error responses. If you're wondering about the significance of creating custom errors, this comprehensive write-up provides a thorough explanation.
IV. Using Custom Errors
With our custom errors primed and ready, let’s modify our earlier middleware example to utilize them:
app.get('/users', async (req, res, next) => {
try {
const users = awaitgetUsersFromDatabase();
res.json(users);
} catch (error) {
if (error instanceofSomeDatabaseSpecificError) {
next(new DatabaseError('Failed to retrieve users from database!'));
} else {
next(error);
}
}
});
Now, when we encounter a database-specific error, we create a new instance of our DatabaseError and pass it to next(). Other types of errors continue to be passed as-is.
V. Customizing Error Responses
Armed with our custom errors, it’s time to polish our error handling middleware to yield appropriate responses based on the error type:
app.use((err, req, res, next) => {
if (err instanceof DatabaseError || err instanceof ValidationError) {
console.error(`${err.name}: ${err.message}`);
res.status(err.statusCode).json({ error: err.message });
} else {
console.error(err);
res.status(500).send('An error occurred!');
}
});
VI. A Comprehensive Example
Let’s consolidate all these techniques into a single application:
class DatabaseError extends Error { /* ... */ }
class ValidationError extends Error { /* ... */ }
const getUsersFromDatabase = async () => { /* ... */ };
const validateRequest = req => { /* ... */ };
app.get('/users', async (req, res, next) => {
try {
validateRequest(req);
const users = await getUsersFromDatabase();
res.json(users);
} catch (error) {
if (error instanceof SomeDatabaseSpecificError) {
next(newDatabaseError('Failed to retrieve users from database!'));
} else if (error instanceof SomeValidationSpecificError) {
next(new ValidationError('Invalid request!'));
} else {
next(error);
}
}
});
app.use((err, req, res, next) => { /* ... */ });
Conclusion
Error handling is an art that demands conscious practice to perfect.
In this article, we’ve examined how to gracefully manage errors in Express.js. By employing custom error classes and designing an Express.js error handling middleware that leverages them, we can generate expressive, context-aware error responses. This approach not only simplifies the user experience but also eases our debugging process.
Until next time, fellow programmers!
For more coding tutorials, please subscribe to my YouTube channel: https://www.youtube.com/@CtrlAltVictoria and Twitter https://twitter.com/ctrlaltvictoria 💕🚀
Posted on August 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024