Why Error Handling Matters
No application is immune to errors. Whether it's a typo in the code, a misconfiguration, or an unforeseen edge case, errors can disrupt user experiences. Effective error handling allows developers to:
- Provide meaningful feedback to users.
- Log and monitor errors for debugging and system improvement.
- Prevent sensitive information from leaking to end users.
Think of error handling like a safety net for a circus performer—while it can't prevent falls, it can minimize the impact.
The Default Error Handler in Express
By default, Express handles errors by returning a response with the error message and stack trace (in non-production environments). Here's an example of what happens when an error occurs:
app.get('/throw-error', (req, res) => {
throw new Error('An unexpected error occurred!');
});
In development, the browser displays the error message along with a detailed stack trace. However, in production (when NODE_ENV is set to "production"), the response is simplified to "Internal Server Error" with a status code of 500. This prevents sensitive details from being exposed.
Custom Error Handlers
The default error handler is sufficient for basic debugging but lacks customization. A custom error handler gives you control over how errors are displayed and logged. Custom error handlers in Express are defined as middleware functions with four parameters:
err: The error object.req: The request object.res: The response object.next: The next middleware function in the chain.
Here's an example of a simple custom error handler:
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).send('An error occurred! Please try again later.');
});
Adding Meaningful Error Responses
To make your application user-friendly, you can customize error messages and status codes. For example:
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500);
res.json({
message: err.message || 'Internal Server Error',
status: res.statusCode,
});
});
This ensures that users receive clear feedback about what went wrong without exposing sensitive information.
Defining Multiple Error Handlers
Sometimes, you may need to handle specific types of errors differently. For example, you might want to log errors to a database in production:
// Error logger
app.use((err, req, res, next) => {
if (process.env.NODE_ENV === 'production') {
// Log the error to a database
console.error('Logged error:', err.message);
} else {
console.error(err);
}
next(err); // Pass the error to the next handler
});
// Generic error handler
app.use((err, req, res, next) => {
res.status(err.status || 500).send('Something went wrong. Please try again later.');
});
Best Practices for Error Handling
- Always define error handlers after all other middleware and route handlers.
- Never expose sensitive details (e.g., stack traces) in production environments.
- Use environment variables (like
NODE_ENV) to differentiate between development and production behavior. - Log errors for monitoring and debugging.
Imagine a car's dashboard warning lights. While they don't fix the issue, they alert the driver to potential problems—your error handlers should function similarly.
Real-World Applications
- Logging errors to a service like Sentry or Datadog for centralized monitoring.
- Returning localized error messages for international users.
- Handling specific error types (e.g., 404 Not Found) with custom responses.
Effective error handling ensures your application is resilient and user-friendly, even when things go wrong.