Skip to main content

Express Error Responses

When building web applications with Express, how you communicate errors to your clients is crucial for providing a good user experience and facilitating effective debugging. This guide will walk you through creating appropriate error responses for various scenarios in your Express applications.

Introduction to Error Responses

Error responses in Express allow you to:

  • Inform clients about what went wrong
  • Provide the appropriate HTTP status code
  • Structure error information consistently
  • Include helpful details for debugging (when appropriate)

Well-crafted error responses help front-end developers handle issues gracefully and make debugging easier for everyone involved.

HTTP Status Codes

HTTP status codes are standardized codes that indicate the result of the client's request to the server. When sending error responses, using the correct status code is essential.

Here are the common error status codes you'll use:

Status CodeNameUse Case
400Bad RequestClient sent invalid data
401UnauthorizedAuthentication required
403ForbiddenClient lacks permission
404Not FoundResource doesn't exist
409ConflictRequest conflicts with current state
422Unprocessable EntityRequest syntax correct but semantically invalid
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer encountered an error

Basic Error Response Structure

In Express, sending an error response is as simple as using the res object with an appropriate status code:

javascript
app.get('/example', (req, res) => {
// Something went wrong
res.status(400).send('Bad Request');
});

However, for APIs, it's better to send structured JSON responses:

javascript
app.get('/api/resource', (req, res) => {
// Something went wrong
res.status(404).json({
error: {
message: 'Resource not found',
code: 'RESOURCE_NOT_FOUND'
}
});
});

Consistent Error Response Format

For better API design, create a consistent error response format across your application. Here's a recommended structure:

javascript
{
error: {
message: "Human-readable error message",
code: "ERROR_CODE",
details: {}, // Optional additional information
stack: "..." // Only in development environments
}
}

Let's implement a helper function to standardize error responses:

javascript
// utils/errorResponse.js
const sendErrorResponse = (res, statusCode, message, errorCode, details = null) => {
const errorResponse = {
error: {
message,
code: errorCode,
}
};

if (details) {
errorResponse.error.details = details;
}

// Only include stack traces in development
if (process.env.NODE_ENV === 'development' && details && details.stack) {
errorResponse.error.stack = details.stack;
}

return res.status(statusCode).json(errorResponse);
};

module.exports = sendErrorResponse;

Now you can use this function throughout your application:

javascript
const sendErrorResponse = require('./utils/errorResponse');

app.get('/api/users/:id', (req, res) => {
try {
const user = findUser(req.params.id);

if (!user) {
return sendErrorResponse(
res,
404,
'User not found',
'USER_NOT_FOUND'
);
}

res.json(user);
} catch (error) {
return sendErrorResponse(
res,
500,
'Server error while retrieving user',
'SERVER_ERROR',
error
);
}
});

Common Error Response Scenarios

1. Missing or Invalid Input

When the client sends invalid data:

javascript
app.post('/api/products', (req, res) => {
const { name, price } = req.body;

if (!name || name.trim() === '') {
return sendErrorResponse(
res,
400,
'Product name is required',
'MISSING_REQUIRED_FIELD',
{ field: 'name' }
);
}

if (typeof price !== 'number' || price <= 0) {
return sendErrorResponse(
res,
400,
'Product price must be a positive number',
'INVALID_FIELD_VALUE',
{ field: 'price', value: price }
);
}

// Process valid input...
});

2. Resource Not Found

When the requested resource doesn't exist:

javascript
app.get('/api/articles/:id', async (req, res) => {
try {
const article = await Article.findById(req.params.id);

if (!article) {
return sendErrorResponse(
res,
404,
`Article with ID ${req.params.id} not found`,
'ARTICLE_NOT_FOUND'
);
}

res.json(article);
} catch (error) {
sendErrorResponse(
res,
500,
'Failed to retrieve article',
'DATABASE_ERROR',
error
);
}
});

3. Authentication and Authorization Errors

When a user isn't authenticated or doesn't have permission:

javascript
app.delete('/api/posts/:id', (req, res) => {
// Authentication check
if (!req.user) {
return sendErrorResponse(
res,
401,
'Authentication required',
'AUTHENTICATION_REQUIRED'
);
}

const post = findPost(req.params.id);

// Authorization check
if (post && post.authorId !== req.user.id) {
return sendErrorResponse(
res,
403,
'You do not have permission to delete this post',
'INSUFFICIENT_PERMISSIONS'
);
}

// Process deletion...
});

4. Validation Errors with Multiple Fields

When handling form validation with multiple possible errors:

javascript
app.post('/api/signup', (req, res) => {
const { email, password, username } = req.body;
const errors = {};

if (!email || !email.includes('@')) {
errors.email = 'Valid email is required';
}

if (!password || password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}

if (!username || username.length < 3) {
errors.username = 'Username must be at least 3 characters';
}

if (Object.keys(errors).length > 0) {
return sendErrorResponse(
res,
422,
'Validation failed',
'VALIDATION_ERROR',
{ fields: errors }
);
}

// Process valid registration...
});

Advanced Error Response Techniques

Custom Error Classes

Creating custom error classes can help standardize error handling throughout your application:

javascript
// errors/ApplicationError.js
class ApplicationError extends Error {
constructor(message, statusCode, errorCode, details = null) {
super(message);
this.statusCode = statusCode;
this.errorCode = errorCode;
this.details = details;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

// Specialized error classes
class NotFoundError extends ApplicationError {
constructor(resource, id, details = null) {
super(
`${resource} with ID ${id} not found`,
404,
`${resource.toUpperCase()}_NOT_FOUND`,
details
);
}
}

class ValidationError extends ApplicationError {
constructor(message, details) {
super(message, 422, 'VALIDATION_ERROR', details);
}
}

module.exports = {
ApplicationError,
NotFoundError,
ValidationError
};

Using these custom errors in your routes:

javascript
const { NotFoundError, ValidationError } = require('./errors/ApplicationError');
const sendErrorResponse = require('./utils/errorResponse');

app.get('/api/orders/:id', async (req, res) => {
try {
const order = await Order.findById(req.params.id);

if (!order) {
throw new NotFoundError('Order', req.params.id);
}

res.json(order);
} catch (error) {
// If it's our custom error
if (error instanceof ApplicationError) {
return sendErrorResponse(
res,
error.statusCode,
error.message,
error.errorCode,
error.details
);
}

// For unexpected errors
return sendErrorResponse(
res,
500,
'An unexpected error occurred',
'SERVER_ERROR',
error
);
}
});

Centralized Error Handling Middleware

Instead of handling errors in each route, you can create a centralized error handling middleware:

javascript
// middlewares/errorHandler.js
const { ApplicationError } = require('../errors/ApplicationError');

const errorHandler = (err, req, res, next) => {
console.error(err.stack);

// Handle custom application errors
if (err instanceof ApplicationError) {
const errorResponse = {
error: {
message: err.message,
code: err.errorCode,
}
};

if (err.details) {
errorResponse.error.details = err.details;
}

if (process.env.NODE_ENV === 'development') {
errorResponse.error.stack = err.stack;
}

return res.status(err.statusCode).json(errorResponse);
}

// Default error response for unexpected errors
const errorResponse = {
error: {
message: 'An unexpected error occurred',
code: 'SERVER_ERROR'
}
};

if (process.env.NODE_ENV === 'development') {
errorResponse.error.stack = err.stack;
}

return res.status(500).json(errorResponse);
};

module.exports = errorHandler;

Register the middleware after all your routes:

javascript
const express = require('express');
const errorHandler = require('./middlewares/errorHandler');
const app = express();

// Your routes
app.get('/api/resource', (req, res, next) => {
try {
// Your logic
if (somethingWentWrong) {
throw new ValidationError('Invalid input', { reason: 'Bad data' });
}

res.json({ success: true });
} catch (error) {
next(error); // Pass the error to the error handler
}
});

// Register the error handler last
app.use(errorHandler);

Best Practices for Error Responses

  1. Use standard HTTP status codes - Use the appropriate status code for each error situation.

  2. Provide clear error messages - Error messages should be human-readable and helpful.

  3. Include error codes - Include machine-readable error codes for programmatic handling.

  4. Be careful with error details - In production, don't expose sensitive internal errors or stack traces.

  5. Log errors - Always log errors on the server side for debugging.

  6. Validate input early - Catch and respond to bad input before processing.

  7. Centralize error handling - Use middleware and helper functions for consistent error responses.

  8. Consider internationalization - For public APIs, consider making error messages translatable.

Real-World Example: REST API for a Blog

Here's a complete example of a blog post endpoint with proper error handling:

javascript
const express = require('express');
const { NotFoundError, ValidationError, ApplicationError } = require('./errors/ApplicationError');
const errorHandler = require('./middlewares/errorHandler');

const app = express();
app.use(express.json());

// GET a post
app.get('/api/posts/:id', async (req, res, next) => {
try {
const postId = req.params.id;

// Validate ID format
if (!postId.match(/^[0-9a-fA-F]{24}$/)) {
throw new ValidationError('Invalid post ID format', { id: postId });
}

const post = await Post.findById(postId);

if (!post) {
throw new NotFoundError('Post', postId);
}

res.json(post);
} catch (error) {
next(error);
}
});

// POST a new post
app.post('/api/posts', async (req, res, next) => {
try {
const { title, content, authorId } = req.body;
const errors = {};

if (!title || title.trim().length < 5) {
errors.title = 'Title must be at least 5 characters';
}

if (!content || content.trim().length < 10) {
errors.content = 'Content must be at least 10 characters';
}

if (!authorId) {
errors.authorId = 'Author ID is required';
}

if (Object.keys(errors).length > 0) {
throw new ValidationError('Invalid post data', { fields: errors });
}

// Check if author exists
const author = await User.findById(authorId);
if (!author) {
throw new ApplicationError(
`Author with ID ${authorId} not found`,
404,
'AUTHOR_NOT_FOUND'
);
}

const newPost = await Post.create({ title, content, authorId });
res.status(201).json(newPost);
} catch (error) {
next(error);
}
});

// Register error handler
app.use(errorHandler);

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

Summary

Creating consistent and informative error responses is an essential part of building robust Express applications. By following the practices outlined in this guide, you can:

  • Provide clear feedback to clients when errors occur
  • Use appropriate HTTP status codes for different error scenarios
  • Structure your error responses consistently
  • Implement centralized error handling for cleaner code
  • Create custom error classes for different error types
  • Balance between helpful error details and security concerns

These techniques will help you build more maintainable and user-friendly Express applications.

Additional Resources

Exercises

  1. Create a custom error handler middleware that formats errors differently based on whether the client requests HTML or JSON responses.

  2. Implement a validation middleware using a library like Joi or express-validator and create appropriate error responses for validation failures.

  3. Build a simple REST API with at least three endpoints that implements all the error handling techniques covered in this guide.

  4. Extend the error handling system to support internationalized error messages.



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