Skip to main content

Express Custom Errors

When building Express applications, you'll inevitably encounter situations where the default Error object doesn't provide enough context or specificity for your error handling needs. Custom errors allow you to create specialized error types that can carry additional information and be handled differently based on their type.

Introduction to Custom Errors

In Express, the default error handling is useful but limited. By creating custom error classes, you can:

  • Differentiate between various types of errors (validation errors, authentication errors, etc.)
  • Include additional data specific to each error type
  • Implement more targeted error handling strategies
  • Provide clearer error messages to both developers and end users

Let's explore how to create and use custom errors in your Express applications.

Creating Custom Error Classes

Custom error classes in JavaScript extend the built-in Error class. Here's a basic pattern for creating custom errors:

javascript
class CustomError extends Error {
constructor(message, statusCode) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}

This base custom error class:

  • Extends the native Error class
  • Sets the error name to the class name
  • Includes a status code for HTTP responses
  • Preserves the stack trace for debugging

Common Custom Error Types for Express

Let's create some specific error types that are useful in Express applications:

javascript
// Base custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode || 500;
this.isOperational = true; // Indicates this is a known operational error
Error.captureStackTrace(this, this.constructor);
}
}

// Specific error types
class ValidationError extends AppError {
constructor(message) {
super(message || 'Validation Error', 400);
this.validationErrors = [];
}

addError(field, message) {
this.validationErrors.push({ field, message });
return this;
}
}

class NotFoundError extends AppError {
constructor(message) {
super(message || 'Resource not found', 404);
}
}

class UnauthorizedError extends AppError {
constructor(message) {
super(message || 'Unauthorized access', 401);
}
}

class ForbiddenError extends AppError {
constructor(message) {
super(message || 'Forbidden access', 403);
}
}

Using Custom Errors in Routes

Once you've defined your custom error classes, you can use them in your Express route handlers:

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

// Import our custom errors
const { NotFoundError, ValidationError, UnauthorizedError } = require('./errors');

app.get('/users/:id', async (req, res, next) => {
try {
const userId = req.params.id;

// Validate user ID
if (!userId.match(/^\d+$/)) {
const validationError = new ValidationError()
.addError('id', 'User ID must be a number');
throw validationError;
}

const user = await findUserById(userId);

// User not found
if (!user) {
throw new NotFoundError(`User with ID ${userId} not found`);
}

// Check authorization
if (user.isPrivate && user.id !== req.user.id) {
throw new UnauthorizedError('You cannot access this user profile');
}

res.json(user);
} catch (error) {
next(error); // Pass errors to the error handling middleware
}
});

Creating an Error Handler Middleware

To handle your custom errors appropriately, create an error handling middleware:

javascript
const errorHandler = (err, req, res, next) => {
// Set default error status and message
const statusCode = err.statusCode || 500;
const message = err.message || 'Something went wrong';

// Prepare the error response
const errorResponse = {
success: false,
status: statusCode,
message: message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
};

// Add validation errors if available
if (err.validationErrors && err.validationErrors.length > 0) {
errorResponse.validationErrors = err.validationErrors;
}

// Log error for server-side debugging
console.error(`[${new Date().toISOString()}] Error:`, err);

// Send response
res.status(statusCode).json(errorResponse);
};

// Add the middleware to your Express app
app.use(errorHandler);

Practical Example: API with Custom Error Handling

Let's see a more complete example of an API with custom error handling:

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

// Custom error classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}

class ValidationError extends AppError {
constructor(message = 'Validation Error', errors = []) {
super(message, 400);
this.validationErrors = errors;
}
}

class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404);
}
}

// Middleware for parsing JSON
app.use(express.json());

// Mock user database
const users = [
{ id: 1, username: 'john_doe', email: '[email protected]' },
{ id: 2, username: 'jane_smith', email: '[email protected]' }
];

// Helper function to find user
const findUser = (id) => users.find(user => user.id === parseInt(id));

// Route to get a user by ID
app.get('/api/users/:id', (req, res, next) => {
try {
const userId = req.params.id;

// Validate user ID format
if (isNaN(parseInt(userId))) {
throw new ValidationError('Invalid user ID format', [
{ field: 'id', message: 'User ID must be a number' }
]);
}

// Find the user
const user = findUser(userId);

// Check if user exists
if (!user) {
throw new NotFoundError(`User with ID ${userId} not found`);
}

// Return the user data
res.json({
success: true,
data: user
});

} catch (error) {
next(error);
}
});

// Create a new user route with validation
app.post('/api/users', (req, res, next) => {
try {
const { username, email } = req.body;
const validationErrors = [];

// Validate username
if (!username || username.length < 3) {
validationErrors.push({
field: 'username',
message: 'Username is required and must be at least 3 characters long'
});
}

// Validate email
if (!email || !email.includes('@')) {
validationErrors.push({
field: 'email',
message: 'A valid email address is required'
});
}

// If validation fails, throw error
if (validationErrors.length > 0) {
throw new ValidationError('Validation failed', validationErrors);
}

// Create new user (in a real app, this would save to a database)
const newUser = {
id: users.length + 1,
username,
email
};

users.push(newUser);

res.status(201).json({
success: true,
data: newUser
});

} catch (error) {
next(error);
}
});

// Error handling middleware
app.use((err, req, res, next) => {
// Set default values
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';

// Prepare response
const errorResponse = {
success: false,
error: {
message,
status: statusCode
}
};

// Add validation errors if available
if (err instanceof ValidationError && err.validationErrors) {
errorResponse.error.validationErrors = err.validationErrors;
}

// Add stack trace in development
if (process.env.NODE_ENV !== 'production') {
errorResponse.error.stack = err.stack;
}

// Send response
res.status(statusCode).json(errorResponse);
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Example Responses

For an invalid user ID:

GET /api/users/abc

Response:
{
"success": false,
"error": {
"message": "Invalid user ID format",
"status": 400,
"validationErrors": [
{
"field": "id",
"message": "User ID must be a number"
}
]
}
}

For a non-existent user:

GET /api/users/999

Response:
{
"success": false,
"error": {
"message": "User with ID 999 not found",
"status": 404
}
}

For invalid new user data:

POST /api/users
Body: { "username": "jo" }

Response:
{
"success": false,
"error": {
"message": "Validation failed",
"status": 400,
"validationErrors": [
{
"field": "username",
"message": "Username is required and must be at least 3 characters long"
},
{
"field": "email",
"message": "A valid email address is required"
}
]
}
}

Best Practices for Custom Errors

When working with custom errors in Express:

  1. Create a hierarchy of error classes - Start with a base class and extend for specific error types
  2. Include appropriate status codes - Each error type should have a default HTTP status code
  3. Add contextual information - Include all relevant data that helps identify and fix the issue
  4. Keep error messages clear - Write user-friendly messages that provide useful information
  5. Log detailed error information - Log full error details on the server but limit what's sent to clients
  6. Handle async errors properly - Use try/catch blocks with async/await or Promise catch handlers
  7. Organize error classes - Store error classes in a separate module for easy reuse

Advanced Custom Error Patterns

For larger applications, consider some of these advanced patterns:

Factory Pattern for Error Creation

javascript
// Error factory
const createError = (type, message, details = {}) => {
switch (type) {
case 'validation':
return new ValidationError(message, details.errors);
case 'notFound':
return new NotFoundError(message);
case 'unauthorized':
return new UnauthorizedError(message);
default:
return new AppError(message, details.statusCode || 500);
}
};

// Usage
const error = createError('validation', 'Invalid input', {
errors: [{ field: 'email', message: 'Invalid email' }]
});

Error with Internationalization Support

javascript
class I18nError extends AppError {
constructor(messageKey, statusCode = 400, params = {}) {
super(messageKey, statusCode);
this.messageKey = messageKey; // Key for translation
this.params = params; // Parameters for translation
}
}

// Error handler middleware with i18n support
app.use((err, req, res, next) => {
if (err instanceof I18nError) {
// Translate message using a translation library
const translatedMessage = translate(req.locale, err.messageKey, err.params);
err.message = translatedMessage;
}

// Continue with normal error handling
res.status(err.statusCode || 500).json({
success: false,
message: err.message
});
});

Summary

Custom errors in Express provide a powerful way to handle different error scenarios in your application. By extending the native Error class, you can create specialized error types with additional properties and behaviors that make your error handling more consistent and informative.

Key takeaways:

  1. Custom errors improve error classification and handling in Express apps
  2. Extend the native Error class to create your custom error types
  3. Include relevant information like status codes and additional context
  4. Use custom errors with Express middleware for centralized error handling
  5. Consider patterns like error hierarchies and factories for larger applications

Additional Resources

Exercises

  1. Create a custom DatabaseError class that includes information about the database operation that failed.
  2. Implement a rate limiting error that includes information about when the user can try again.
  3. Build an error handler that formats errors differently based on whether the request wants HTML or JSON.
  4. Create a validation error system that works with a form validation library of your choice.
  5. Extend the error handling middleware to send different messages in development vs. production environments.


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