Error Handling and Error Reporting in Express.js

Handling errors in Express.js applications is crucial for a reliable, user-friendly, and maintainable application.

Introduction to Error Handling in Express.js

Error handling ensures that any unexpected issues that arise in your Express.js application are managed gracefully. By properly handling errors, you can prevent the application from crashing and offer users clear, helpful messages when something goes wrong.

Basic Error Handling Middleware

In Express.js, middleware functions can be used to handle errors at different points in the request lifecycle. Basic error-handling middleware uses four parameters: err, req, res, and next.

Example of Basic Error Handling Middleware

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

// Sample route that throws an error
app.get('/error', (req, res, next) => {
    next(new Error('Something went wrong!'));
});

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);  // Log error details for debugging
    res.status(500).json({ message: 'Internal Server Error' });
});

app.listen(3000, () => {
    console.log('Server running on http://localhost:3000');
});

				
			

Explanation:

  • In this example, an error is intentionally created in the /error route.
  • The error-handling middleware catches any errors passed through next().
  • res.status(500) sets the HTTP status to 500 (Internal Server Error), and a JSON response is sent.

Output: When visiting http://localhost:3000/error, you’ll see {"message": "Internal Server Error"} and the error details will be printed in the console.

Custom Error Handling Middleware

Express allows you to create custom error handlers to manage different types of errors uniquely. This enables you to tailor error responses based on the type of error.

Example of Custom Error Handler for 404 Errors

				
					// 404 handler middleware for unknown routes
app.use((req, res, next) => {
    res.status(404).json({ message: 'Resource not found' });
});

				
			

This middleware catches all requests that don’t match any defined route, returning a 404 status code with a custom message.

Handling Asynchronous Errors

In asynchronous code, such as in async functions or when working with Promises, errors may not automatically propagate. You can handle these with try-catch blocks or use a wrapper function to handle errors in a DRY (Don’t Repeat Yourself) way.

Example of Asynchronous Error Handling

				
					app.get('/async-error', async (req, res, next) => {
    try {
        // Simulate an asynchronous error
        const result = await someAsyncFunction();
        res.json(result);
    } catch (error) {
        next(error); // Pass the error to the error-handling middleware
    }
});

				
			

In this example, any errors thrown in someAsyncFunction() are caught by catch, and next(error) sends it to the error-handling middleware.

Error Reporting and Logging

Logging errors is critical for diagnosing issues in production. The morgan package can log HTTP requests, and winston can log error details to files or external services.

Setting Up Logging with Morgan and Winston

				
					npm install morgan winston

				
			
				
					const morgan = require('morgan');
const winston = require('winston');

// Initialize morgan for request logging
app.use(morgan('combined'));

// Initialize winston for error logging
const logger = winston.createLogger({
    transports: [
        new winston.transports.File({ filename: 'errors.log' })
    ]
});

app.use((err, req, res, next) => {
    logger.error(err.message); // Log error message to errors.log
    res.status(500).json({ message: 'Internal Server Error' });
});

				
			

Explanation:

  • morgan logs all HTTP requests, which is helpful for debugging.
  • winston logs error messages to errors.log, providing a record of issues for future reference.

Creating Custom Error Classes

Custom error classes help categorize errors and add more information, making them easier to handle differently based on the error type.

Example of a Custom Error Class

				
					class NotFoundError extends Error {
    constructor(message) {
        super(message);
        this.status = 404;
    }
}

app.get('/user/:id', (req, res, next) => {
    const user = findUserById(req.params.id);
    if (!user) {
        return next(new NotFoundError('User not found'));
    }
    res.json(user);
});

app.use((err, req, res, next) => {
    res.status(err.status || 500).json({ message: err.message });
});

				
			

Explanation:

  • The NotFoundError class extends Error, allowing you to set a custom status property for 404 errors.
  • The error-handling middleware sends the custom message and status.

Output: If the user is not found, the response will be {"message": "User not found"}, with a 404 status.

Client-Friendly Error Responses

When building public APIs, it’s essential to provide user-friendly, descriptive error messages.

Example of User-Friendly Error Responses

				
					app.use((err, req, res, next) => {
    if (err instanceof ValidationError) {
        return res.status(400).json({ error: err.message, field: err.field });
    }
    res.status(err.status || 500).json({ message: 'Something went wrong, please try again later' });
});

				
			

Explanation:

  • This middleware sends a user-friendly error response, hiding internal details from users.

Using Error Tracking Services

Integrate error-tracking services like Sentry or Rollbar for real-time error monitoring.

Setting Up Sentry for Error Tracking

Install Sentry:

				
					npm install @sentry/node

				
			

Configure Sentry:

				
					npm install @sentry/node
const Sentry = require('@sentry/node');
Sentry.init({ dsn: 'YOUR_SENTRY_DSN' });

app.use(Sentry.Handlers.errorHandler());

// Example route to trigger error
app.get('/sentry-error', (req, res) => {
    throw new Error('Sentry test error');
});

				
			

Explanation:

  • Sentry logs errors remotely, enabling you to monitor issues as they arise in production.

Testing Error Handling

Testing your error handlers ensures they work as expected.

Example of Testing Error Handling with Jest

				
					// Example Jest test
test('GET /error should return 500', async () => {
    const response = await request(app).get('/error');
    expect(response.status).toBe(500);
    expect(response.body.message).toBe('Internal Server Error');
});

				
			

Effective error handling in Express.js is essential for building reliable, maintainable applications. By implementing basic and custom error-handling middleware, logging errors with tools like Winston, and integrating error-tracking services like Sentry, you can provide users with a better experience and quickly diagnose issues. Happy Coding!❤️

Table of Contents