← Back to all tutorials

Error Handling

Implement robust error handling in Express — catch validation errors, handle missing resources, and create a centralized error middleware.

Error Handling

A production REST API must handle errors gracefully. Bad input, missing resources, database failures — your API should respond with clear error messages and appropriate status codes instead of crashing or returning cryptic errors.

Types of Errors

Error TypeStatus CodeExample
Validation error400Missing required fields, invalid data types
Not found404Requesting a resource that does not exist
Cast error400Invalid MongoDB ObjectId format
Duplicate key409Creating a resource that already exists (unique field)
Server error500Database connection lost, unexpected crash

Try-Catch in Route Handlers

router.get('/:id', async (req, res) => {
    try {
        const ninja = await Ninja.findById(req.params.id);
        if (!ninja) {
            return res.status(404).json({ error: 'Ninja not found' });
        }
        res.json(ninja);
    } catch (err) {
        if (err.name === 'CastError') {
            return res.status(400).json({ error: 'Invalid ID format' });
        }
        res.status(500).json({ error: 'Server error' });
    }
});

Centralized Error Middleware

// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
    console.error(err.stack);

    // Mongoose validation error
    if (err.name === 'ValidationError') {
        const messages = Object.values(err.errors).map(e => e.message);
        return res.status(400).json({ errors: messages });
    }

    // Mongoose cast error (invalid ObjectId)
    if (err.name === 'CastError') {
        return res.status(400).json({ error: 'Invalid ID format' });
    }

    // Duplicate key error
    if (err.code === 11000) {
        return res.status(409).json({ error: 'Duplicate key error' });
    }

    // Default server error
    res.status(500).json({ error: 'Internal server error' });
}

module.exports = errorHandler;

Error-handling middleware has four parameters(err, req, res, next). Express recognizes the four-parameter signature and only calls it when an error is passed to next().

Using the Error Middleware

// index.js
const errorHandler = require('./middleware/errorHandler');

app.use('/api/ninjas', ninjaRoutes);

// Error middleware must come AFTER routes
app.use(errorHandler);

Passing Errors to the Middleware

router.post('/', async (req, res, next) => {
    try {
        const ninja = new Ninja(req.body);
        const savedNinja = await ninja.save();
        res.status(201).json(savedNinja);
    } catch (err) {
        next(err);  // Passes the error to the error middleware
    }
});

Instead of handling errors in every route, call next(err) to forward the error to the centralized error handler. This keeps route handlers clean and ensures consistent error responses.

404 Handler (Route Not Found)

// After all routes, before error middleware
app.use((req, res) => {
    res.status(404).json({ error: 'Route not found' });
});

This middleware catches any request that did not match a defined route and returns a 404 response.

Key Takeaways

  • Always wrap async route handlers in try-catch and call next(err) to forward errors
  • Error middleware has four parameters (err, req, res, next) and must be defined after all routes
  • Handle specific error types (validation, cast, duplicate) with appropriate status codes
  • A centralized error handler ensures consistent error responses across the entire API
  • Always add a 404 catch-all middleware after all routes for unmatched URLs