Express Error Basics
Error handling is a critical aspect of building reliable Express.js applications. When something goes wrong in your application, proper error handling ensures that your server responds appropriately rather than crashing or leaving users with a confusing experience.
Introduction to Error Handling in Express
In any web application, errors are inevitable. Users might submit invalid data, external services may become unavailable, or your code might contain bugs. Express provides several mechanisms to catch and handle these errors gracefully.
By default, Express comes with a built-in error handler, but understanding how to customize error handling gives you more control over how your application behaves when things go wrong.
Types of Errors in Express Applications
Before diving into handling errors, let's understand the common types of errors you might encounter:
- Operational errors - These occur during normal operation (e.g., user input errors, network failures)
- Programming errors - Bugs in your code (e.g., trying to read properties of undefined)
- Express-specific errors - Like route not found errors
Default Error Handling in Express
Express comes with a built-in error handler that takes care of any errors that might occur in your application.
Here's how Express handles errors by default:
// This route will cause an error
app.get('/error-example', (req, res) => {
// Attempting to access a property of undefined
const name = undefined.property;
res.send(`Hello, ${name}!`);
});
When this route is accessed, Express catches the error and:
- Sends a HTTP 500 status code
- Prints the error stack trace if in development mode
- Sends a simple error message to the client
Output in development:
TypeError: Cannot read property 'property' of undefined
at /app/server.js:12:23
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
... (more stack trace)
Output in production: Simply shows: "Internal Server Error"
Catching Synchronous Errors
Express automatically catches and processes synchronous errors thrown in route handlers and middleware:
app.get('/synchronous-error', (req, res) => {
// This error will be caught automatically
throw new Error('Something went wrong!');
});
When you access this route, Express catches the error and passes it to the default or custom error handler.
Handling Asynchronous Errors
Asynchronous errors require special handling since they can't be caught by Express's default error catching mechanism.
The Problem with Async Errors
app.get('/async-error', async (req, res) => {
// This error won't be caught by default!
await someAsyncFunction(); // Assuming this function throws an error
res.send('Success!');
});
If someAsyncFunction()
throws an error, it won't be caught by Express, potentially causing your application to crash.
Solutions for Async Errors
Option 1: Try/Catch
app.get('/async-error-handled', async (req, res, next) => {
try {
await someAsyncFunction();
res.send('Success!');
} catch (error) {
next(error); // Pass error to Express
}
});
Option 2: Promise Chaining
app.get('/promise-error', (req, res, next) => {
Promise.resolve()
.then(() => {
throw new Error('Promise-based error');
})
.catch(next); // Pass error to Express
});
Option 3: Express Async Handler
Many developers use utilities like express-async-handler
to simplify async error handling:
const asyncHandler = require('express-async-handler');
app.get('/clean-async', asyncHandler(async (req, res) => {
// Error here will be caught and passed to error handlers
const result = await someAsyncFunction();
res.json(result);
}));
Creating a Basic Error Handler
To customize how Express handles errors, you can define your own error handling middleware:
const express = require('express');
const app = express();
// Regular routes
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.get('/error', (req, res) => {
throw new Error('Sample error');
});
// Error handling middleware (must be defined last)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send({
error: {
message: err.message,
status: 500
}
});
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
Error handling middleware is distinguished from regular middleware by having four parameters instead of three (err
, req
, res
, next
).
404 Not Found Errors
A special case of error handling is dealing with requests to nonexistent routes:
// Define all your routes first
// ...
// Then add this middleware for handling 404s
app.use((req, res, next) => {
res.status(404).send({
error: {
message: 'Route not found',
status: 404
}
});
});
// Then your error handling middleware
app.use((err, req, res, next) => {
// ...
});
Practical Example: Building a Complete Error Handling System
Let's create a more comprehensive error handling system that:
- Creates custom error types
- Handles different errors differently
- Formats errors appropriately for API responses
const express = require('express');
const app = express();
// Custom error classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Middleware to handle body parsing errors
app.use(express.json({
limit: '10kb',
// This runs if express.json() fails to parse JSON
verify: (req, res, buf) => {
try {
JSON.parse(buf);
} catch (e) {
res.status(400).json({
status: 'fail',
message: 'Invalid JSON'
});
throw new AppError('Invalid JSON', 400);
}
}
}));
// Sample route that might throw an error
app.get('/api/users/:id', (req, res, next) => {
const id = req.params.id;
// Simulating a database lookup
if (id === '999') {
return next(new AppError('User not found', 404));
}
if (id === '0') {
return next(new Error('Database connection failed')); // Non-operational error
}
res.json({ id, name: 'Sample User' });
});
// 404 handler for undefined routes
app.all('*', (req, res, next) => {
next(new AppError(`Cannot find ${req.originalUrl} on this server!`, 404));
});
// Global error handler
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// Different handling for development vs production
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else {
// For production, send clean error
if (err.isOperational) {
// Send operational error details
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
// Don't leak error details for programming errors
console.error('ERROR 💥', err);
res.status(500).json({
status: 'error',
message: 'Something went wrong'
});
}
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Let's test our error handling:
- Accessing
/api/users/123
returns a normal user response - Accessing
/api/users/999
returns a 404 error with a custom message - Accessing
/api/users/0
triggers a simulated internal error - Accessing any undefined route returns a custom 404 message
Best Practices for Express Error Handling
- Centralize error handling - Use a single error handling middleware
- Differentiate between operational and programming errors
- Log errors appropriately - Log detailed information for debugging
- Send appropriate error responses - Format errors according to your API standards
- Don't leak error details in production - Hide stack traces from users
- Use custom error classes - For better organization and handling
- Handle all promise rejections - Ensure no unhandled promise rejections
- Add validation - Validate input data to catch errors early
Summary
Express error handling is a critical part of building robust applications. By understanding how Express processes errors and implementing proper error handling middleware, you can:
- Prevent application crashes
- Provide better feedback to users
- Make debugging easier
- Create a more professional user experience
The key points to remember are:
- Express has a default error handler
- Synchronous errors are caught automatically
- Asynchronous errors need special handling
- Custom error handlers give you more control
- Different types of errors may need different handling strategies
Additional Resources
- Express.js Error Handling Documentation
- Node.js Error Handling Practices
- MDN Web Docs: HTTP response status codes
Exercises
- Create a simple Express app with a route that throws different errors based on query parameters
- Implement a custom error handling middleware that formats errors as JSON
- Add handling for asynchronous errors using try/catch and next()
- Create a custom error class for validation errors
- Implement different error responses for development and production environments
By mastering Express error handling fundamentals, you'll build more robust, user-friendly applications that gracefully handle unexpected situations rather than crashing when problems occur.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)