Understanding JavaScript's Error System
JavaScript provides a built-in Error class that serves as the foundation for all errors in the language. Before diving into custom error classes for Express applications, it's essential to understand how JavaScript's native error system works.
Native JavaScript Error Types
JavaScript includes several built-in error types that extend the base Error class:
- Error: The generic error class, base for all other errors
- SyntaxError: Raised when JavaScript code has syntax issues
- ReferenceError: Raised when referencing undefined variables
- TypeError: Raised when a value is not of the expected type
- RangeError: Raised when a value is outside the allowable range
- URIError: Raised when encoding/decoding URIs incorrectly
- EvalError: Raised when using the eval() function improperly
- AggregateError: Represents multiple errors as a single error (newer)
JavaScript's Native Error Structure
// Creating a basic error
const error = new Error('Something went wrong');
console.log(error.message); // 'Something went wrong'
console.log(error.name); // 'Error'
console.log(error.stack); // Stack trace showing where the error occurred
// Other error types
const typeError = new TypeError('Expected string but got number');
console.log(typeError.name); // 'TypeError'
console.log(typeError.message); // 'Expected string but got number'
// Try/catch example
try {
const obj = null;
console.log(obj.property); // This will throw a TypeError
} catch (error) {
console.log(error.name); // 'TypeError'
console.log(error.message); // 'Cannot read property 'property' of null'
// We can determine the type of error
if (error instanceof TypeError) {
console.log('This is a type error');
}
}
Think of JavaScript's error system as a family tree. The Error class is the parent, with specialized children (TypeError, RangeError, etc.) that handle specific error situations. When you create custom error classes, you're essentially extending this family to include new specialized members tailored to your application's needs.
Why Create Custom Error Classes?
While JavaScript's built-in error types are useful, they don't cover all the specific error scenarios you might encounter in a web application. Custom error classes provide several benefits:
Benefits of Custom Error Classes
- Better Error Classification: Group errors by type for easier handling
- Additional Context: Include domain-specific information with errors
- Consistent Error Format: Standardize error responses across the application
- Improved Debugging: More precise error details for faster problem resolution
- Code Readability: More expressive error types make code intention clearer
- Standardized Status Codes: Associate HTTP status codes directly with error types
- Client-friendly Errors: Create error messages suitable for end-users
Custom error classes act as translators between your application's internal errors and the responses sent to clients. They help categorize, enrich, and standardize error information, making your application more maintainable and user-friendly.
Creating Custom Error Classes in JavaScript
Creating custom error classes in JavaScript is straightforward. You extend the built-in Error class and add your own properties and methods.
Basic Custom Error Class
// Basic custom error class
class CustomError extends Error {
constructor(message) {
super(message); // Call parent constructor
this.name = 'CustomError'; // Set the error name
// Capture stack trace, excluding the constructor call from it
Error.captureStackTrace(this, this.constructor);
}
}
// Using the custom error
try {
throw new CustomError('This is a custom error');
} catch (error) {
console.log(error.name); // 'CustomError'
console.log(error.message); // 'This is a custom error'
console.log(error.stack); // Stack trace starting from where the error was thrown
}
Adding Custom Properties
// Custom error with additional properties
class ValidationError extends Error {
constructor(message, field, value) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.value = value;
this.timestamp = new Date();
Error.captureStackTrace(this, this.constructor);
}
// Custom method
toJSON() {
return {
error: {
type: this.name,
message: this.message,
field: this.field,
value: this.value,
timestamp: this.timestamp
}
};
}
}
// Using the validation error
try {
const age = -5;
if (age < 0) {
throw new ValidationError('Age must be a positive number', 'age', age);
}
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.field); // 'age'
console.log(error.value); // -5
console.log(error.timestamp); // Current date and time
console.log(error.toJSON()); // Formatted error object
}
}
About Error.captureStackTrace
The Error.captureStackTrace(this, this.constructor) line is important for custom errors. It:
- Creates a
.stackproperty on the error object - Omits the error constructor function itself from the stack trace
- Makes the stack trace start from where the error was thrown, not created
- Helps with debugging by providing cleaner stack traces
V8-specific feature (works in Node.js and Chromium-based browsers), but gracefully ignored in other environments.
Custom Error Classes for Express Applications
When building Express applications, it's helpful to create a hierarchy of error classes tailored to web API scenarios.
Base Application Error
Start with a base error class that all other custom errors will extend. This class should include common properties relevant to HTTP APIs:
Base Application Error Class
// errors/AppError.js
class AppError extends Error {
constructor(message, statusCode = 500, type = 'SERVER_ERROR', isOperational = true) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.type = type;
this.isOperational = isOperational; // Flag to distinguish operational vs programmer errors
this.timestamp = new Date();
Error.captureStackTrace(this, this.constructor);
}
/**
* Converts the error to a format suitable for API responses
*/
toJSON() {
return {
error: {
type: this.type,
message: this.message,
statusCode: this.statusCode,
timestamp: this.timestamp
}
};
}
}
module.exports = AppError;
The isOperational Flag
The isOperational flag is a common pattern for distinguishing between two types of errors:
- Operational Errors (isOperational = true): Expected problems that occur during normal operation (e.g., invalid user input, resource not found, authentication failure)
- Programmer Errors (isOperational = false): Bugs in the code that should be fixed rather than handled at runtime (e.g., TypeError from trying to access a property of undefined)
This distinction helps with error handling strategies - operational errors are handled gracefully, while programmer errors often trigger alerts for developers.
Common Express API Error Classes
Let's build a hierarchy of error classes useful for Express APIs:
Common API Error Classes
// errors/index.js
const AppError = require('./AppError');
/**
* Validation Error (400 Bad Request)
* Used for input validation failures
*/
class ValidationError extends AppError {
constructor(message = 'Validation failed', invalidFields = []) {
super(message, 400, 'VALIDATION_ERROR');
this.invalidFields = invalidFields;
}
toJSON() {
const json = super.toJSON();
json.error.invalidFields = this.invalidFields;
return json;
}
}
/**
* Not Found Error (404 Not Found)
* Used when a requested resource doesn't exist
*/
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
this.resource = resource;
}
toJSON() {
const json = super.toJSON();
json.error.resource = this.resource;
return json;
}
}
/**
* Authentication Error (401 Unauthorized)
* Used for authentication failures
*/
class AuthenticationError extends AppError {
constructor(message = 'Authentication failed') {
super(message, 401, 'AUTHENTICATION_ERROR');
}
}
/**
* Authorization Error (403 Forbidden)
* Used when a user lacks permission for an action
*/
class AuthorizationError extends AppError {
constructor(message = 'Not authorized') {
super(message, 403, 'AUTHORIZATION_ERROR');
}
}
/**
* Database Error (500 Internal Server Error)
* Used for database operation failures
*/
class DatabaseError extends AppError {
constructor(message = 'Database operation failed', operation = '', originalError = null) {
super(message, 500, 'DATABASE_ERROR');
this.operation = operation;
this.originalError = originalError;
}
toJSON() {
const json = super.toJSON();
json.error.operation = this.operation;
// Don't include the original error in JSON output
// as it might contain sensitive information
return json;
}
}
/**
* Rate Limit Error (429 Too Many Requests)
* Used when a client exceeds rate limits
*/
class RateLimitError extends AppError {
constructor(message = 'Rate limit exceeded', retryAfter = 60) {
super(message, 429, 'RATE_LIMIT_EXCEEDED');
this.retryAfter = retryAfter;
}
toJSON() {
const json = super.toJSON();
json.error.retryAfter = this.retryAfter;
return json;
}
}
/**
* Conflict Error (409 Conflict)
* Used when a resource already exists or conflicts with another
*/
class ConflictError extends AppError {
constructor(message = 'Resource conflict', conflictField = '') {
super(message, 409, 'CONFLICT_ERROR');
this.conflictField = conflictField;
}
toJSON() {
const json = super.toJSON();
if (this.conflictField) {
json.error.conflictField = this.conflictField;
}
return json;
}
}
/**
* Service Unavailable Error (503 Service Unavailable)
* Used when a service is temporarily unavailable
*/
class ServiceUnavailableError extends AppError {
constructor(message = 'Service temporarily unavailable', retryAfter = null) {
super(message, 503, 'SERVICE_UNAVAILABLE');
this.retryAfter = retryAfter;
}
toJSON() {
const json = super.toJSON();
if (this.retryAfter) {
json.error.retryAfter = this.retryAfter;
}
return json;
}
}
/**
* Bad Gateway Error (502 Bad Gateway)
* Used when an upstream service returns an invalid response
*/
class BadGatewayError extends AppError {
constructor(message = 'Bad gateway', service = '') {
super(message, 502, 'BAD_GATEWAY');
this.service = service;
}
toJSON() {
const json = super.toJSON();
if (this.service) {
json.error.service = this.service;
}
return json;
}
}
module.exports = {
AppError,
ValidationError,
NotFoundError,
AuthenticationError,
AuthorizationError,
DatabaseError,
RateLimitError,
ConflictError,
ServiceUnavailableError,
BadGatewayError
};
Using Custom Error Classes in Express Routes
Now that we've defined our error classes, let's see how to use them effectively in Express routes.
Using Custom Errors in Routes
// routes/users.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const {
NotFoundError,
ValidationError,
AuthenticationError,
ConflictError
} = require('../errors');
// Get user by ID
router.get('/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
// Use NotFoundError for missing resources
throw new NotFoundError('User');
}
res.json(user);
} catch (err) {
next(err); // Pass error to error handling middleware
}
});
// Create new user
router.post('/', async (req, res, next) => {
try {
const { email, username, password } = req.body;
// Validate required fields
const invalidFields = [];
if (!email) {
invalidFields.push({ field: 'email', message: 'Email is required' });
}
if (!username) {
invalidFields.push({ field: 'username', message: 'Username is required' });
}
if (!password || password.length < 8) {
invalidFields.push({ field: 'password', message: 'Password must be at least 8 characters' });
}
if (invalidFields.length > 0) {
// Use ValidationError for invalid input
throw new ValidationError('Invalid user data', invalidFields);
}
// Check if email already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
// Use ConflictError for duplicate resources
throw new ConflictError('Email already in use', 'email');
}
// Create user
const user = await User.create({ email, username, password });
res.status(201).json(user);
} catch (err) {
next(err);
}
});
// Login user
router.post('/login', async (req, res, next) => {
try {
const { email, password } = req.body;
// Validate required fields
if (!email || !password) {
throw new ValidationError('Email and password are required');
}
// Find user by email
const user = await User.findOne({ email });
if (!user) {
// Use AuthenticationError for login failures
// Note: In real applications, avoid revealing whether the email exists
throw new AuthenticationError('Invalid email or password');
}
// Check password
const isMatch = await user.comparePassword(password);
if (!isMatch) {
throw new AuthenticationError('Invalid email or password');
}
// Generate token and respond
const token = user.generateAuthToken();
res.json({ token });
} catch (err) {
next(err);
}
});
module.exports = router;
Error Handler Middleware
// middleware/errorHandler.js
const { AppError } = require('../errors');
// Development error handler - with details
const developmentErrorHandler = (err, req, res, next) => {
console.error(err);
// Set status code
const statusCode = err.statusCode || 500;
// Prepare response
let errorResponse = {
error: {
message: err.message || 'Internal Server Error',
type: err.type || 'SERVER_ERROR',
stack: err.stack
}
};
// Add additional fields if available
if (err instanceof AppError) {
errorResponse = err.toJSON();
errorResponse.error.stack = err.stack;
}
res.status(statusCode).json(errorResponse);
};
// Production error handler - without stack traces and sensitive info
const productionErrorHandler = (err, req, res, next) => {
// Log error for server monitoring
console.error(err);
// Set status code
const statusCode = err.statusCode || 500;
// Operational errors can be sent to client with details
if (err instanceof AppError && err.isOperational) {
return res.status(statusCode).json(err.toJSON());
}
// For non-operational errors, send a generic message
res.status(500).json({
error: {
message: 'Something went wrong. Please try again later.',
type: 'SERVER_ERROR'
}
});
};
// Choose the appropriate error handler based on environment
const errorHandler = process.env.NODE_ENV === 'production'
? productionErrorHandler
: developmentErrorHandler;
module.exports = errorHandler;
Setting Up the Error Handler
Don't forget to register the error handler in your main Express application:
Registering Error Handler in Express App
// app.js
const express = require('express');
const errorHandler = require('./middleware/errorHandler');
const userRoutes = require('./routes/users');
const app = express();
// Middleware
app.use(express.json());
// Routes
app.use('/api/users', userRoutes);
// ... other routes
// 404 handler - for routes that don't exist
app.use((req, res, next) => {
const error = new NotFoundError('Route');
next(error);
});
// Global error handler - must be last middleware
app.use(errorHandler);
module.exports = app;
Advanced Error Handling Patterns
Let's explore some advanced patterns for working with custom error classes in Express applications.
Converting Third-party Errors
It's common to work with libraries and services that throw their own error types. You can create wrapper functions to convert these to your custom error classes:
Converting Database Errors
// utils/errorHandlers.js
const {
ValidationError,
DatabaseError,
NotFoundError,
ConflictError
} = require('../errors');
/**
* Handles Mongoose/MongoDB errors and converts them to custom errors
*/
const handleMongooseError = (err) => {
// Validation errors
if (err.name === 'ValidationError') {
const invalidFields = Object.keys(err.errors).map(field => ({
field,
message: err.errors[field].message
}));
return new ValidationError('Validation failed', invalidFields);
}
// Duplicate key error (unique constraint violation)
if (err.name === 'MongoError' && err.code === 11000) {
const field = Object.keys(err.keyPattern)[0];
return new ConflictError(`${field} already exists`, field);
}
// Cast errors (invalid ID format, etc.)
if (err.name === 'CastError') {
if (err.kind === 'ObjectId') {
return new ValidationError(`Invalid ${err.path} format`);
}
}
// Default to DatabaseError
return new DatabaseError('Database operation failed', '', err);
};
/**
* Wraps database operations with error conversion
*/
const withErrorHandling = (fn) => async (...args) => {
try {
return await fn(...args);
} catch (err) {
throw handleMongooseError(err);
}
};
module.exports = {
handleMongooseError,
withErrorHandling
};
Using the Error Handler Wrapper
// services/userService.js
const User = require('../models/User');
const { withErrorHandling } = require('../utils/errorHandlers');
const { NotFoundError } = require('../errors');
// Original function
const findUserById = async (id) => {
const user = await User.findById(id);
if (!user) {
throw new NotFoundError('User');
}
return user;
};
// Wrapped function with error handling
const findUserByIdSafe = withErrorHandling(findUserById);
// Usage in routes
router.get('/:id', async (req, res, next) => {
try {
// Any Mongoose errors will be converted to custom errors
const user = await findUserByIdSafe(req.params.id);
res.json(user);
} catch (err) {
next(err);
}
});
Error Factory Pattern
For more complex applications, an error factory pattern can help create consistent errors:
Error Factory Implementation
// utils/errorFactory.js
const {
ValidationError,
NotFoundError,
AuthenticationError,
AuthorizationError,
DatabaseError,
ConflictError
} = require('../errors');
/**
* Factory for creating common error types
*/
const ErrorFactory = {
/**
* Create a validation error
*/
validation: (message, fields = []) => {
return new ValidationError(message, fields);
},
/**
* Create a not found error
*/
notFound: (resource) => {
return new NotFoundError(resource);
},
/**
* Create an authentication error
*/
authentication: (message = 'Authentication failed') => {
return new AuthenticationError(message);
},
/**
* Create an authorization error
*/
authorization: (message = 'Not authorized') => {
return new AuthorizationError(message);
},
/**
* Create a database error
*/
database: (message, operation, originalError) => {
return new DatabaseError(message, operation, originalError);
},
/**
* Create a conflict error
*/
conflict: (message, field) => {
return new ConflictError(message, field);
}
};
module.exports = ErrorFactory;
Using the Error Factory
// routes/products.js
const express = require('express');
const router = express.Router();
const Product = require('../models/Product');
const ErrorFactory = require('../utils/errorFactory');
router.get('/:id', async (req, res, next) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
throw ErrorFactory.notFound('Product');
}
res.json(product);
} catch (err) {
next(err);
}
});
router.post('/', async (req, res, next) => {
try {
const { name, price, description } = req.body;
// Validate fields
const invalidFields = [];
if (!name) {
invalidFields.push({ field: 'name', message: 'Product name is required' });
}
if (!price || isNaN(price) || price <= 0) {
invalidFields.push({ field: 'price', message: 'Price must be a positive number' });
}
if (invalidFields.length > 0) {
throw ErrorFactory.validation('Invalid product data', invalidFields);
}
// Check if product name already exists
const existingProduct = await Product.findOne({ name });
if (existingProduct) {
throw ErrorFactory.conflict('Product name already exists', 'name');
}
// Create product
const product = await Product.create({ name, price, description });
res.status(201).json(product);
} catch (err) {
next(err);
}
});
Async Error Wrapper
To avoid repetitive try/catch blocks in your route handlers, you can create a wrapper function:
Async Route Handler Wrapper
// utils/asyncHandler.js
/**
* Wraps an async route handler and forwards errors to Express error middleware
*
* @param {Function} fn - Async route handler function
* @returns {Function} Wrapped route handler
*/
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;
Using the Async Handler
// routes/users.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const { NotFoundError } = require('../errors');
const asyncHandler = require('../utils/asyncHandler');
// Get user by ID - without try/catch
router.get('/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('User');
}
res.json(user);
}));
// Create user - without try/catch
router.post('/', asyncHandler(async (req, res) => {
const { email, username, password } = req.body;
// Validation logic...
const user = await User.create({ email, username, password });
res.status(201).json(user);
}));
module.exports = router;
Domain-Specific Error Classes
In real-world applications, you might need more specialized error classes for your domain:
Domain-Specific Errors for E-commerce
// errors/ecommerce.js
const { AppError } = require('./index');
/**
* Payment Error (402 Payment Required)
* Used for payment processing failures
*/
class PaymentError extends AppError {
constructor(message = 'Payment failed', code = '', provider = '') {
super(message, 402, 'PAYMENT_ERROR');
this.code = code;
this.provider = provider;
}
toJSON() {
const json = super.toJSON();
json.error.code = this.code;
json.error.provider = this.provider;
return json;
}
}
/**
* Inventory Error (400 Bad Request)
* Used when items are out of stock
*/
class InventoryError extends AppError {
constructor(message = 'Inventory issue', productId = '', currentStock = 0) {
super(message, 400, 'INVENTORY_ERROR');
this.productId = productId;
this.currentStock = currentStock;
}
toJSON() {
const json = super.toJSON();
json.error.productId = this.productId;
json.error.currentStock = this.currentStock;
return json;
}
}
/**
* Shipping Error (400 Bad Request)
* Used for shipping-related issues
*/
class ShippingError extends AppError {
constructor(message = 'Shipping error', code = '', address = null) {
super(message, 400, 'SHIPPING_ERROR');
this.code = code;
this.address = address;
}
toJSON() {
const json = super.toJSON();
json.error.code = this.code;
if (this.address) {
json.error.address = this.address;
}
return json;
}
}
/**
* Coupon Error (400 Bad Request)
* Used for invalid or expired coupons
*/
class CouponError extends AppError {
constructor(message = 'Coupon error', code = '', couponCode = '') {
super(message, 400, 'COUPON_ERROR');
this.code = code;
this.couponCode = couponCode;
}
toJSON() {
const json = super.toJSON();
json.error.code = this.code;
json.error.couponCode = this.couponCode;
return json;
}
}
module.exports = {
PaymentError,
InventoryError,
ShippingError,
CouponError
};
Using Domain-Specific Errors
// routes/orders.js
const express = require('express');
const router = express.Router();
const Order = require('../models/Order');
const Product = require('../models/Product');
const { ValidationError } = require('../errors');
const {
PaymentError,
InventoryError,
ShippingError
} = require('../errors/ecommerce');
const asyncHandler = require('../utils/asyncHandler');
// Create order
router.post('/', asyncHandler(async (req, res) => {
const { products, shippingAddress, paymentMethod } = req.body;
// Validate input
if (!products || !Array.isArray(products) || products.length === 0) {
throw new ValidationError('Products list is required and must not be empty');
}
if (!shippingAddress) {
throw new ValidationError('Shipping address is required');
}
if (!paymentMethod) {
throw new ValidationError('Payment method is required');
}
// Check inventory for each product
for (const item of products) {
const product = await Product.findById(item.productId);
if (!product) {
throw new ValidationError(`Product with ID ${item.productId} not found`);
}
if (product.stock < item.quantity) {
throw new InventoryError(
`Not enough stock for ${product.name}`,
product._id.toString(),
product.stock
);
}
}
// Validate shipping address
if (!isValidAddress(shippingAddress)) {
throw new ShippingError(
'Invalid shipping address',
'INVALID_ADDRESS',
shippingAddress
);
}
// Process payment
try {
await processPayment(paymentMethod, calculateTotal(products));
} catch (error) {
throw new PaymentError(
'Payment processing failed',
error.code || 'PAYMENT_FAILED',
paymentMethod.provider
);
}
// Create order
const order = await Order.create({
user: req.user._id,
products,
shippingAddress,
paymentMethod,
total: calculateTotal(products),
status: 'processing'
});
// Update inventory
for (const item of products) {
await Product.findByIdAndUpdate(item.productId, {
$inc: { stock: -item.quantity }
});
}
res.status(201).json(order);
}));
// Helper functions
function isValidAddress(address) {
// Address validation logic
return (
address &&
address.street &&
address.city &&
address.state &&
address.zipCode &&
address.country
);
}
function calculateTotal(products) {
// Total calculation logic
// ...
return total;
}
async function processPayment(paymentMethod, amount) {
// Payment processing logic
// ...
}
module.exports = router;
Testing Custom Error Classes
It's important to test your custom error classes to ensure they work as expected:
Unit Testing Error Classes
// tests/errors.test.js
const { expect } = require('chai');
const {
AppError,
ValidationError,
NotFoundError,
AuthenticationError
} = require('../errors');
describe('Custom Error Classes', () => {
describe('AppError', () => {
it('should create a base error with default properties', () => {
const error = new AppError('Test error');
expect(error).to.be.instanceOf(Error);
expect(error).to.be.instanceOf(AppError);
expect(error.name).to.equal('AppError');
expect(error.message).to.equal('Test error');
expect(error.statusCode).to.equal(500);
expect(error.type).to.equal('SERVER_ERROR');
expect(error.isOperational).to.be.true;
expect(error.stack).to.exist;
});
it('should accept custom statusCode and type', () => {
const error = new AppError('Custom error', 418, 'IM_A_TEAPOT');
expect(error.statusCode).to.equal(418);
expect(error.type).to.equal('IM_A_TEAPOT');
});
it('should convert to JSON correctly', () => {
const error = new AppError('Test error', 500, 'TEST_ERROR');
const json = error.toJSON();
expect(json).to.have.property('error');
expect(json.error).to.have.property('type', 'TEST_ERROR');
expect(json.error).to.have.property('message', 'Test error');
expect(json.error).to.have.property('statusCode', 500);
expect(json.error).to.have.property('timestamp');
});
});
describe('ValidationError', () => {
it('should create a validation error with status code 400', () => {
const invalidFields = [
{ field: 'email', message: 'Invalid email' },
{ field: 'password', message: 'Password too short' }
];
const error = new ValidationError('Validation failed', invalidFields);
expect(error).to.be.instanceOf(AppError);
expect(error).to.be.instanceOf(ValidationError);
expect(error.statusCode).to.equal(400);
expect(error.type).to.equal('VALIDATION_ERROR');
expect(error.invalidFields).to.deep.equal(invalidFields);
});
it('should include invalidFields in JSON output', () => {
const invalidFields = [
{ field: 'email', message: 'Invalid email' }
];
const error = new ValidationError('Validation failed', invalidFields);
const json = error.toJSON();
expect(json.error).to.have.property('invalidFields');
expect(json.error.invalidFields).to.deep.equal(invalidFields);
});
});
describe('NotFoundError', () => {
it('should create a not found error with status code 404', () => {
const error = new NotFoundError('User');
expect(error).to.be.instanceOf(AppError);
expect(error.statusCode).to.equal(404);
expect(error.type).to.equal('NOT_FOUND');
expect(error.message).to.equal('User not found');
expect(error.resource).to.equal('User');
});
});
describe('AuthenticationError', () => {
it('should create an authentication error with status code 401', () => {
const error = new AuthenticationError();
expect(error).to.be.instanceOf(AppError);
expect(error.statusCode).to.equal(401);
expect(error.type).to.equal('AUTHENTICATION_ERROR');
expect(error.message).to.equal('Authentication failed');
});
it('should accept a custom message', () => {
const error = new AuthenticationError('Invalid token');
expect(error.message).to.equal('Invalid token');
});
});
});
Integration Testing Error Handling
// tests/integration/error-handling.test.js
const request = require('supertest');
const { expect } = require('chai');
const app = require('../../app');
const User = require('../../models/User');
describe('Error Handling Integration Tests', () => {
// Clean up database before tests
beforeEach(async () => {
await User.deleteMany({});
});
describe('Not Found Errors', () => {
it('should return 404 with proper error format for non-existent route', async () => {
const res = await request(app)
.get('/api/non-existent-route')
.expect('Content-Type', /json/)
.expect(404);
expect(res.body).to.have.property('error');
expect(res.body.error).to.have.property('type', 'NOT_FOUND');
expect(res.body.error).to.have.property('message', 'Route not found');
expect(res.body.error).to.have.property('statusCode', 404);
});
it('should return 404 with proper error format for non-existent user', async () => {
const res = await request(app)
.get('/api/users/60a1b3d5c5b4c0a3e8f0b5a1') // Non-existent ID
.expect('Content-Type', /json/)
.expect(404);
expect(res.body).to.have.property('error');
expect(res.body.error).to.have.property('type', 'NOT_FOUND');
expect(res.body.error).to.have.property('message', 'User not found');
expect(res.body.error).to.have.property('statusCode', 404);
expect(res.body.error).to.have.property('resource', 'User');
});
});
describe('Validation Errors', () => {
it('should return 400 with validation errors for invalid user creation', async () => {
const res = await request(app)
.post('/api/users')
.send({
// Missing required fields
})
.expect('Content-Type', /json/)
.expect(400);
expect(res.body).to.have.property('error');
expect(res.body.error).to.have.property('type', 'VALIDATION_ERROR');
expect(res.body.error).to.have.property('invalidFields').that.is.an('array');
expect(res.body.error.invalidFields).to.have.length.greaterThan(0);
});
});
describe('Authentication Errors', () => {
it('should return 401 for invalid login credentials', async () => {
// First create a user
await User.create({
email: 'test@example.com',
username: 'testuser',
password: 'password123'
});
// Try to login with wrong password
const res = await request(app)
.post('/api/users/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
})
.expect('Content-Type', /json/)
.expect(401);
expect(res.body).to.have.property('error');
expect(res.body.error).to.have.property('type', 'AUTHENTICATION_ERROR');
expect(res.body.error).to.have.property('message', 'Invalid email or password');
expect(res.body.error).to.have.property('statusCode', 401);
});
});
});
Best Practices
Here are some best practices for working with custom error classes in Express applications:
Best Practices] --> B[Create a Base
Error Class] A --> C[Include HTTP
Status Codes] A --> D[Add Context
Information] A --> E[Distinguish Operational
from Programming Errors] A --> F[Standardize
JSON Format] A --> G[Centralize
Error Handling] A --> H[Test Your
Error Classes]
Error Hierarchy
- Keep it Simple - Don't create too many error classes; focus on common use cases
- Inheritance - Use inheritance to share common functionality
- Naming - Use descriptive names that reflect HTTP status codes or domain concepts
Error Properties
- HTTP Status Codes - Include appropriate status codes for API responses
- Error Types - Use string constants for error categorization
- Context - Include relevant context information (IDs, field names, etc.)
- isOperational - Distinguish between expected operational errors and programmer errors
Error Handling
- Centralized Handler - Use a single error handling middleware
- Environment-specific - Provide different error responses in development and production
- Security - Hide sensitive information in production responses
- Logging - Log all errors for monitoring and debugging
Code Organization
- Modularity - Keep error classes in separate files or modules
- Helper Functions - Create utility functions to reduce boilerplate
- Consistent Patterns - Use consistent error handling patterns throughout the application
- Documentation - Document your error classes and their usage
Practical Exercises
Exercise 1: Basic Error Classes
Create a hierarchy of custom error classes for a RESTful API:
- Create a base
AppErrorclass that extendsError - Create specialized error classes for common HTTP status codes (400, 401, 403, 404, 409, 500)
- Implement a central error handling middleware that formats errors appropriately
- Create a small Express application with routes that demonstrate throwing and handling these errors
Test your implementation with both valid and invalid requests.
Exercise 2: Advanced Domain Errors
Build on Exercise 1 to create a more advanced error handling system for a specific domain (e.g., e-commerce, blogging, social media):
- Create domain-specific error classes (e.g., PaymentError, InventoryError, etc.)
- Implement error factories to create errors consistently
- Create utility functions to handle try/catch boilerplate
- Implement converter functions for third-party errors (e.g., database errors)
- Add unit tests for your error classes
Implement a small API with routes that use these error classes and demonstrate proper error handling.
Additional Resources
Documentation and Articles
- Node.js Error Documentation
- Express.js Error Handling Guide
- MDN Error Object Documentation
- Joyent's Guide to Error Handling in Node.js
Libraries
- Node.js Best Practices - Error Handling
- Micro - Error Handling
- Express Validator
- Multer - Multipart File Handling