When building a web application, we need to handle cases where users request resources that don't exist. Think of this like a customer asking for a product that's not in our store - we want to give them a friendly and helpful message rather than a confusing technical error.
Our specific goals are:
1. Replace Express's default "Cannot GET /path" message with a custom error for unknown routes
2. Create a middleware to detect when no routes match a request
3. Implement an error handler to format error responses consistently
4. Ensure the root route (/) still works correctly
// First, we keep our existing root route
app.get('/', (req, res) => {
res.send('GET / This is the root URL');
});
// Resource not found middleware - comes after all route handlers
app.use((req, res, next) => {
// Create error object with custom message
const err = new Error('Sorry, the requested resource couldn\'t be found');
// Add status code to indicate not found
err.statusCode = 404;
// Pass error to error handling middleware
next(err);
});
// Catch-all error handler - must be last middleware
app.use((err, req, res, next) => {
// Log error for server-side debugging
console.error(err);
// Set status code, defaulting to 500 if none specified
const statusCode = err.statusCode || 500;
// Send formatted error response
res.status(statusCode).json({
message: err.message,
statusCode: statusCode
});
});
Let's break down how this works, like following a package through a postal system:
1. When a request arrives, it first tries to match defined routes (like checking addressed mailboxes)
2. If no route matches, it reaches our resource-not-found middleware (like the "undeliverable mail" department)
3. This middleware creates an error object (like filling out a "failed delivery" form)
4. The error gets passed to our error handler (like the customer service department)
5. The error handler formats a friendly response (like writing an explanation letter to the sender)
This pattern is used in many ways beyond just 404 errors:
- E-commerce: Handling requests for out-of-stock or discontinued products
- Social media: Responding to requests for deleted posts or deactivated accounts
- Content management: Managing requests for unpublished or archived content
- API services: Providing meaningful feedback for invalid endpoints
// Enhanced error handler with more details
app.use((err, req, res, next) => {
console.error(err);
const statusCode = err.statusCode || 500;
const response = {
message: err.message,
statusCode: statusCode,
path: req.path,
timestamp: new Date().toISOString(),
suggestions: []
};
// Add helpful suggestions based on error type
if (statusCode === 404) {
response.suggestions = [
'Check the URL for typos',
'Make sure you\'re using the correct HTTP method',
'Verify that the resource hasn\'t been moved or deleted'
];
}
res.status(statusCode).json(response);
});
When implementing error handling, consider these principles:
1. Security: Don't expose sensitive error details in production
2. Consistency: Use the same error format across your entire application
3. Helpfulness: Provide guidance on how to resolve the error
4. Logging: Keep detailed server-side logs for debugging
To verify your implementation, test these cases:
1. GET / → Should show root URL message
2. GET /unknown → Should show 404 error response
3. GET /api/unknown → Should show 404 error response
4. POST / → Should show 404 error response (if no POST handler exists)
Think of error handling like a safety net in a circus - it should catch all falls gracefully and ensure everyone (both users and developers) knows what happened and what to do next.