Swiftorial Logo
Home
Swift Lessons
Matchups
CodeSnaps
Tutorials
Career
Resources

Express.js and Advanced Error Handling

Advanced error handling in Express.js helps ensure that your application can gracefully handle errors, log them appropriately, and provide meaningful responses to clients. This guide covers key concepts, examples, and best practices for implementing advanced error handling in Express.js applications.

Key Concepts of Advanced Error Handling

  • Middleware: Functions that handle requests and responses in Express.js. Error-handling middleware functions have four arguments: err, req, res, and next.
  • Error Objects: Custom error objects that provide additional context and information about errors.
  • Logging: Recording error details to track and diagnose issues.
  • Client Responses: Sending meaningful error messages and appropriate HTTP status codes to clients.
  • Graceful Shutdown: Ensuring the application can gracefully handle and recover from errors without crashing.

Setting Up the Project

Initialize a new Express.js project and install necessary dependencies:

// Initialize a new project
// npm init -y

// Install Express
// npm install express

// Create the project structure
// mkdir src
// touch src/index.js src/errorHandler.js

Creating Custom Error Objects

Create custom error objects to provide additional context and information about errors:

Example: CustomError.js

// src/CustomError.js
class CustomError extends Error {
    constructor(status, message, isOperational = true, stack = '') {
        super(message);
        this.status = status;
        this.isOperational = isOperational;
        if (stack) {
            this.stack = stack;
        } else {
            Error.captureStackTrace(this, this.constructor);
        }
    }
}

module.exports = CustomError;

Creating an Error Handler Middleware

Create an error handler middleware to handle errors globally:

Example: errorHandler.js

// src/errorHandler.js
const CustomError = require('./CustomError');

const errorHandler = (err, req, res, next) => {
    let { status, message, isOperational } = err;
    status = status || 500;
    message = message || 'Internal Server Error';

    if (!isOperational) {
        console.error('Unexpected Error:', err);
    }

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

module.exports = errorHandler;

Using the Error Handler Middleware

Use the error handler middleware in your Express.js application:

Example: index.js

// src/index.js
const express = require('express');
const CustomError = require('./CustomError');
const errorHandler = require('./errorHandler');

const app = express();
const port = 3000;

app.use(express.json());

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

app.get('/error', (req, res, next) => {
    next(new CustomError(400, 'This is a custom error'));
});

// Use the error handler middleware
app.use(errorHandler);

app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}/`);
});

Logging Errors

Use a logging library to record error details for tracking and diagnosing issues:

Example: Logging with Winston

// Install Winston
// npm install winston

// src/logger.js
const { createLogger, format, transports } = require('winston');

const logger = createLogger({
    level: 'error',
    format: format.combine(
        format.timestamp(),
        format.json()
    ),
    transports: [
        new transports.File({ filename: 'error.log', level: 'error' }),
        new transports.Console({ format: format.simple() })
    ]
});

module.exports = logger;

// src/errorHandler.js (modified)
const logger = require('./logger');

const errorHandler = (err, req, res, next) => {
    let { status, message, isOperational } = err;
    status = status || 500;
    message = message || 'Internal Server Error';

    if (!isOperational) {
        logger.error('Unexpected Error:', { message: err.message, stack: err.stack });
    }

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

module.exports = errorHandler;

Handling Uncaught Exceptions and Rejections

Handle uncaught exceptions and unhandled promise rejections to prevent the application from crashing:

Example: Handling Uncaught Exceptions and Rejections

// src/index.js (additional code)
process.on('uncaughtException', (err) => {
    logger.error('Uncaught Exception:', { message: err.message, stack: err.stack });
    process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
    logger.error('Unhandled Rejection:', { reason, promise });
    process.exit(1);
});

Best Practices for Advanced Error Handling

  • Use Custom Error Objects: Create custom error objects to provide additional context and information about errors.
  • Implement Global Error Handling: Use an error handler middleware to handle errors globally.
  • Log Errors: Use a logging library to record error details for tracking and diagnosing issues.
  • Handle Uncaught Exceptions and Rejections: Handle uncaught exceptions and unhandled promise rejections to prevent the application from crashing.
  • Provide Meaningful Client Responses: Send meaningful error messages and appropriate HTTP status codes to clients.
  • Monitor and Update: Continuously monitor error logs and update your error handling strategies to improve application stability and user experience.

Testing Error Handling

Test your error handling strategies to ensure they work correctly and efficiently:

Example: Testing with Mocha and Chai

// Install Mocha and Chai
// npm install --save-dev mocha chai

// test/errorHandling.test.js
const chai = require('chai');
const expect = chai.expect;
const request = require('supertest');
const app = require('../src/index'); // Adjust this to the correct path of your index.js

describe('Error Handling', () => {
    it('should handle custom errors', (done) => {
        request(app)
            .get('/error')
            .expect(400)
            .end((err, res) => {
                if (err) return done(err);
                expect(res.body.message).to.equal('This is a custom error');
                done();
            });
    });

    it('should handle 404 errors', (done) => {
        request(app)
            .get('/nonexistent')
            .expect(404, done);
    });
});

// Define test script in package.json
// "scripts": {
//   "test": "mocha"
// }

// Run tests with NPM
// npm run test

Key Points

  • Middleware: Functions that handle requests and responses in Express.js. Error-handling middleware functions have four arguments: err, req, res, and next.
  • Error Objects: Custom error objects that provide additional context and information about errors.
  • Logging: Recording error details to track and diagnose issues.
  • Client Responses: Sending meaningful error messages and appropriate HTTP status codes to clients.
  • Graceful Shutdown: Ensuring the application can gracefully handle and recover from errors without crashing.
  • Follow best practices for advanced error handling, such as using custom error objects, implementing global error handling, logging errors, handling uncaught exceptions and rejections, providing meaningful client responses, and monitoring and updating your error handling strategies.

Conclusion

Advanced error handling in Express.js helps ensure that your application can gracefully handle errors, log them appropriately, and provide meaningful responses to clients. By understanding and implementing the key concepts, examples, and best practices covered in this guide, you can effectively implement advanced error handling in your Express.js applications. Happy coding!