Understanding the Problem
In this phase, we need to set up error handling middleware for our Express application. Error handling is crucial for a robust API as it allows us to:
- Catch and respond to requests for non-existent routes
- Format Sequelize database errors consistently
- Format and customize error responses for better client-side handling
Without proper error handling, our API would return generic responses that aren't helpful for debugging or client-side error handling. Good error handling provides clear feedback about what went wrong and potentially how to fix it.
Planning the Solution
- Create a "Resource Not Found" middleware to catch unhandled routes
- Create a Sequelize error handler middleware to format database errors
- Create a general error formatter middleware to standardize error responses
- Test the error handling by attempting to access non-existent routes
We'll implement these middlewares in our app.js file, after the routes are connected. Remember that the order of middlewares in Express is important - the error handling middlewares should be defined last.
Implementing the Solution
1. Import Required Dependencies
First, we need to import the ValidationError class from Sequelize. Add this to the top of your app.js file with the other imports:
// backend/app.js
// ... other imports
const { ValidationError } = require('sequelize');
2. Resource Not Found Error-Handler
This middleware will catch any requests that don't match any of your defined routes. It creates a 404 error with a descriptive message. Add this after your routes are connected (after app.use(routes)):
// Catch unhandled requests and forward to error handler.
app.use((_req, _res, next) => {
const err = new Error("The requested resource couldn't be found.");
err.title = "Resource Not Found";
err.errors = { message: "The requested resource couldn't be found." };
err.status = 404;
next(err);
});
Let's break down what's happening in this middleware:
- The underscore prefix in
_reqand_resindicates that these parameters are not used in the function. - We create a new Error object with a descriptive message.
- We add custom properties to the error object:
title,errors, andstatus. - Finally, we call
next(err)to pass the error to the next error-handling middleware.
3. Sequelize Error-Handler
This middleware specifically handles Sequelize validation errors, formatting them in a consistent way. Add this after the "Resource Not Found" middleware:
// Process sequelize errors
app.use((err, _req, _res, next) => {
// check if error is a Sequelize error:
if (err instanceof ValidationError) {
let errors = {};
for (let error of err.errors) {
errors[error.path] = error.message;
}
err.title = 'Validation error';
err.errors = errors;
}
next(err);
});
This middleware checks if the error is an instance of Sequelize's ValidationError. If it is, it reformats the error messages into a more user-friendly format, where each property corresponds to a field with a validation error. For example:
{
"email": "Email is required",
"username": "Username must be at least 4 characters long"
}
4. Error Formatter Middleware
The final error handler middleware formats all errors into a consistent JSON response. Add this after the Sequelize error handler:
// Error formatter
app.use((err, _req, res, _next) => {
res.status(err.status || 500);
console.error(err);
res.json({
title: err.title || 'Server Error',
message: err.message,
errors: err.errors,
stack: isProduction ? null : err.stack
});
});
This middleware does several important things:
- Sets the HTTP status code of the response based on the error's status property (defaulting to 500 if not specified)
- Logs the error to the console for server-side debugging
- Formats the response as JSON with consistent fields: title, message, errors, and stack
- Only includes the stack trace in development environments, not in production
This creates a consistent error response format across your entire API, making it easier for clients to handle errors.
5. Complete Error Handling Implementation
Your app.js file should now have all three error handling middlewares added after the routes. Make sure they're in the correct order:
// backend/app.js
const express = require('express');
require('express-async-errors');
const morgan = require('morgan');
const cors = require('cors');
const csurf = require('csurf');
const helmet = require('helmet');
const cookieParser = require('cookie-parser');
const { ValidationError } = require('sequelize');
const { environment } = require('./config');
const isProduction = environment === 'production';
const routes = require('./routes');
const app = express();
// Connect morgan middleware for logging
app.use(morgan('dev'));
// Parse cookies and JSON bodies
app.use(cookieParser());
app.use(express.json());
// Security middleware
if (!isProduction) {
// Enable CORS only in development
app.use(cors());
}
// Helmet helps set security headers
app.use(
helmet.crossOriginResourcePolicy({
policy: "cross-origin"
})
);
// Set the _csrf token and create req.csrfToken method
app.use(
csurf({
cookie: {
secure: isProduction,
sameSite: isProduction && "Lax",
httpOnly: true
}
})
);
// Connect routes
app.use(routes);
// Catch unhandled requests and forward to error handler.
app.use((_req, _res, next) => {
const err = new Error("The requested resource couldn't be found.");
err.title = "Resource Not Found";
err.errors = { message: "The requested resource couldn't be found." };
err.status = 404;
next(err);
});
// Process sequelize errors
app.use((err, _req, _res, next) => {
// check if error is a Sequelize error:
if (err instanceof ValidationError) {
let errors = {};
for (let error of err.errors) {
errors[error.path] = error.message;
}
err.title = 'Validation error';
err.errors = errors;
}
next(err);
});
// Error formatter
app.use((err, _req, res, _next) => {
res.status(err.status || 500);
console.error(err);
res.json({
title: err.title || 'Server Error',
message: err.message,
errors: err.errors,
stack: isProduction ? null : err.stack
});
});
module.exports = app;
6. Testing the Error Handlers
Let's test our error handling middleware to ensure it's working correctly. Start your server if it's not already running:
cd backend
npm start
Now, try accessing a route that doesn't exist in your application. Open your browser and navigate to:
http://localhost:8000/not-found
You should see a JSON response like:
{
"title": "Resource Not Found",
"message": "The requested resource couldn't be found.",
"errors": {
"message": "The requested resource couldn't be found."
},
"stack": "Error: The requested resource couldn't be found.\n ..."
}
This confirms that your "Resource Not Found" error handler is working correctly.
Also, make sure your CSRF route is still working by navigating to:
http://localhost:8000/api/csrf/restore
You should see a JSON response with a CSRF token, indicating that your regular routes are still functioning correctly alongside the error handlers.
We'll test the Sequelize error handler later once we have implemented database models.
Review the Solution
Let's review what we've accomplished in this phase:
- Created a "Resource Not Found" middleware that catches requests to non-existent routes
- Implemented a Sequelize error handler that formats database validation errors
- Added a general error formatter middleware that standardizes all error responses
- Successfully tested the "Resource Not Found" error handler
Why This Approach Works Well
This error handling approach has several advantages:
- Centralized error handling: Instead of handling errors in each route handler, we've centralized the logic, making it easier to maintain.
- Consistent error responses: All errors now follow the same format, making it easier for client applications to handle them.
- Environment-aware: We only include stack traces in development environments, improving security in production.
- Specialized handling: Different types of errors (like Sequelize validation errors) get specialized formatting.
Real-world Application
In real-world applications, robust error handling is crucial for several reasons:
- User experience: Clear error messages help users understand what went wrong and potentially how to fix it.
- Debugging: Detailed error information in development environments makes it easier to debug issues.
- Security: Proper error handling prevents leaking sensitive information in production.
- API consistency: A consistent error format makes it easier for client applications to handle errors.
For example, in a user registration system, when a user tries to register with an email that's already taken, they should receive a clear message like "Email is already in use" rather than a generic error or, worse, a stack trace revealing implementation details.
Common Issues and Solutions
- Error handlers not being called: Make sure your error handlers are defined after all your routes and middleware.
- Routes still working after error handlers: If you define routes after your error handlers, they won't be caught by the "Resource Not Found" middleware.
- Sequelize errors not being formatted: Ensure you're importing ValidationError from sequelize and checking for instanceof correctly.
- Stack traces appearing in production: Double-check that your isProduction variable is correctly set based on the NODE_ENV environment variable.
Next Steps
Now that you have set up error handling for your application, you are ready to move on to the next phase: Phase 3 User Authentication. In that phase, you'll:
- Create a Users table with Sequelize
- Set up user model with validations
- Implement authentication utilities with JWT
- Create authentication middleware
With solid error handling in place, you'll be able to provide clear feedback to users when authentication-related issues occur.