Skip to main content

Express Error Handling

Error handling is a critical aspect of building robust web applications. When working with Express.js, proper error handling ensures that your application can gracefully handle unexpected issues without crashing and provide meaningful feedback to users. In this guide, we'll explore various strategies and best practices for handling errors in Express applications.

Why Error Handling Matters

Proper error handling offers several benefits:

  1. Improved User Experience: Users receive helpful messages instead of confusing technical errors
  2. Application Stability: Your application continues running even when errors occur
  3. Better Debugging: Structured error handling makes it easier to identify and fix issues
  4. Security: Prevents sensitive error information from being exposed to users

Express Default Error Handler

Express comes with a built-in error handler that takes care of any errors that might occur in your app. However, this default handler is very basic and primarily intended for development environments.

If an error occurs in your application and you don't handle it manually, Express's default error handler will:

  1. Set the HTTP status code to 500 (Internal Server Error)
  2. Send a basic error response to the client
  3. Log the error stack trace to the console

Here's how an error might be triggered without proper handling:

javascript
app.get('/error-example', (req, res) => {
// This will trigger an error because nonExistentFunction is not defined
nonExistentFunction();
res.send('This will not be executed');
});

When a user visits /error-example, they'll receive a response that looks like this (in development):

Error: nonExistentFunction is not defined
at /path/to/your/app.js:XX:X
at Layer.handle [as handle_request] (/path/to/node_modules/express/lib/router/layer.js:95:5)
...

In production, a simpler error page is shown, but it's still not ideal for users.

Creating Custom Error Handlers

Basic Error Handling Middleware

Express uses middleware for error handling. An error-handling middleware is defined with four parameters instead of the usual three (err, req, res, next):

javascript
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});

Here's how you can implement a basic error handler:

javascript
const express = require('express');
const app = express();

// Regular route
app.get('/', (req, res) => {
res.send('Hello World!');
});

// Route with an error
app.get('/broken', (req, res, next) => {
// Simulate an error
try {
throw new Error('Something went wrong!');
} catch (error) {
next(error); // Pass the error to Express
}
});

// Error handling middleware (always keep these at the end)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke! Please try again later.');
});

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

When a user visits /broken, instead of the app crashing, they'll see the message "Something broke! Please try again later."

Creating Custom Error Classes

For more structured error handling, you can create custom error classes:

javascript
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);
}
}

module.exports = AppError;

Then you can use this custom error class in your routes:

javascript
const AppError = require('./utils/appError');

app.get('/not-found', (req, res, next) => {
next(new AppError('Resource not found', 404));
});

app.get('/unauthorized', (req, res, next) => {
next(new AppError('You are not authorized to access this resource', 401));
});

// Error handling middleware
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';

res.status(err.statusCode).json({
status: err.status,
message: err.message
});
});

Handling Async Errors

Handling errors in asynchronous code requires special attention. When using async/await, you need to make sure errors are properly caught and passed to Express.

Without Helper Function

javascript
app.get('/async', async (req, res, next) => {
try {
// Some async operation that might fail
const result = await someAsyncOperation();
res.json(result);
} catch (error) {
next(error); // Pass error to Express
}
});

With Helper Function

Writing try/catch blocks everywhere can be repetitive. You can create a wrapper function to handle asynchronous errors:

javascript
// Helper function to wrap async handlers
const catchAsync = fn => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};

// Now you can use it to simplify your route handlers
app.get('/async', catchAsync(async (req, res) => {
const result = await someAsyncOperation();
res.json(result);
}));

Different Types of Error Responses

Depending on your application's needs, you might want to handle different types of errors differently:

API Error Responses

For API endpoints, you'll typically want to return JSON:

javascript
app.use((err, req, res, next) => {
// Check if the request is expecting JSON
if (req.xhr || req.headers.accept.includes('application/json')) {
return res.status(err.statusCode || 500).json({
status: 'error',
message: err.message
});
}

// Otherwise, respond with HTML
res.status(err.statusCode || 500).render('error', {
message: err.message,
error: process.env.NODE_ENV === 'development' ? err : {}
});
});

HTML Error Pages

For web applications serving HTML, you might want to render an error page:

javascript
app.use((err, req, res, next) => {
res.status(err.statusCode || 500);
res.render('error', {
title: 'Something went wrong',
message: err.message,
// Only provide error details in development
error: process.env.NODE_ENV === 'development' ? err : {}
});
});

Environment-Specific Error Handling

It's important to handle errors differently based on your environment:

javascript
app.use((err, req, res, next) => {
// Set default values
const statusCode = err.statusCode || 500;
const message = err.message || 'Something went wrong';

// For development, include the stack trace and full error
if (process.env.NODE_ENV === 'development') {
return res.status(statusCode).json({
status: 'error',
message,
stack: err.stack,
error: err
});
}

// For production, send a minimal error response
// Also consider logging the full error to your monitoring system
res.status(statusCode).json({
status: 'error',
message: statusCode === 500 ? 'Something went wrong on our end' : message
});
});

Handling Common HTTP Errors

404 Not Found

A special case of error handling is dealing with 404 errors (routes that don't exist):

javascript
// This should be placed after all your defined routes
app.all('*', (req, res, next) => {
const err = new Error(`Can't find ${req.originalUrl} on this server!`);
err.statusCode = 404;
next(err);
});

// Or if you're using the custom AppError class:
app.all('*', (req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
});

Validation Errors

When using libraries like Mongoose or Express-validator, you might want to handle validation errors separately:

javascript
app.use((err, req, res, next) => {
if (err.name === 'ValidationError') {
// Handle mongoose validation errors
const messages = Object.values(err.errors).map(val => val.message);
return res.status(400).json({
status: 'fail',
message: 'Invalid input data',
details: messages
});
}

next(err);
});

Real-World Application: Complete Error Handling Setup

Here's a complete example combining the concepts we've discussed:

javascript
const express = require('express');
const AppError = require('./utils/appError');

const app = express();

// Helper function for async route handlers
const catchAsync = fn => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};

// Routes
app.get('/', (req, res) => {
res.send('Welcome to our API');
});

app.get('/api/products', catchAsync(async (req, res) => {
const products = await fetchProductsFromDB();
res.json({
status: 'success',
results: products.length,
data: { products }
});
}));

app.get('/api/products/:id', catchAsync(async (req, res, next) => {
const product = await findProductById(req.params.id);

if (!product) {
return next(new AppError(`No product found with ID ${req.params.id}`, 404));
}

res.json({
status: 'success',
data: { product }
});
}));

// Handle undefined routes
app.all('*', (req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
});

// Global error handling middleware
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';

// Different handling for development and 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
// Don't leak error details for server errors
let error = { ...err };
error.message = err.message;

// Specific error handling for common cases
if (error.name === 'CastError') {
error = handleCastErrorDB(error);
}
if (error.code === 11000) {
error = handleDuplicateFieldsDB(error);
}
if (error.name === 'ValidationError') {
error = handleValidationErrorDB(error);
}

// Send the response
res.status(error.statusCode).json({
status: error.status,
message: error.message
});
}
});

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

// Example error handling functions
function handleCastErrorDB(err) {
const message = `Invalid ${err.path}: ${err.value}`;
return new AppError(message, 400);
}

function handleDuplicateFieldsDB(err) {
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
const message = `Duplicate field value: ${value}. Please use another value!`;
return new AppError(message, 400);
}

function handleValidationErrorDB(err) {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
}

Summary

Proper error handling is essential for building robust Express applications. By implementing custom error handlers, you can:

  • Provide meaningful feedback to users
  • Keep your application running even when errors occur
  • Maintain different error handling strategies for development and production
  • Handle common errors in a structured way
  • Implement consistent error responses across your API

Remember these key principles:

  1. Create custom error classes for different error types
  2. Use middleware for centralized error handling
  3. Handle async errors with try/catch or helper functions
  4. Adapt error responses based on the environment
  5. Provide appropriate information based on the error type

Additional Resources

Exercises

  1. Create a simple Express application with different routes that trigger various types of errors (404, validation error, server error).
  2. Implement a custom error class that can handle different error types.
  3. Extend the error handler to send different responses based on whether the client expects HTML or JSON.
  4. Implement a logging mechanism that records errors to a file but only shows limited information to users.
  5. Create a middleware that handles errors from a database connection and provides a user-friendly message.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)