DEV Community

Cover image for Standardizing Express.js Error Handling with One Library
Nse569h
Nse569h

Posted on • Edited on

Standardizing Express.js Error Handling with One Library

If you've been working with Express.js error handling for a while, you know the struggle. What starts as a simple project quickly turns into controllers filled with repetitive try/catch blocks and scattered status code logic:

// The "Bad" Way
app.post('/users', async (req, res) => {
  try {
    // ... logic
  } catch (error) {
    console.error(error);
    if (error.code === 'P2002') { // Prisma unique constraint
        return res.status(400).json({ message: 'User already exists' });
    }
    // ... 10 more if/else checks
    res.status(500).json({ message: 'Something went wrong' });
  }
});
Enter fullscreen mode Exit fullscreen mode

It's repetitive, hard to maintain, and prone to bugs. I got tired of copy-pasting error handling logic between my projects, so I built a tool to fix it once and for all.


Meet ds-express-errors -> zero dependencies library!


Why you need centralized error handling in Express.js

Express developers often search for best practices to avoid repetitive try/catch blocks, inconsistent status codes, and unreadable controller logic. ds-express-errors solves these issues by providing a structured, predictable, and type-safe error handling flow for Express.js applications.

​Here is why I think it's worth checking out:

Ready-to-use Error Presets for Express.js 🛠️

​Forget about remembering if "Forbidden" is 401 or 403. Just use the presets.

import { Errors } from 'ds-express-errors';

if (!user) {
  throw Errors.NotFound('User not found'); // Automatically sends 404
}

if (!user.hasAccess) {
  throw Errors.Forbidden('Access denied'); // Automatically sends 403
}
Enter fullscreen mode Exit fullscreen mode

Auto-Mapping for Zod, Joi, Prisma, Mongoose, Sequelize, JWT Errors 🪄

​This is my favorite part. If you use Zod, Joi, Prisma, Mongoose, Sequelize, JWT, the library automatically detects their native errors and converts them into readable HTTP 400 Bad Requests.

Example with Zod:
You don't need to manually parse error.issues.

Input (Invalid):

{
    "issues": [
        {
            "path": ["user", "email"],
            "message": "Invalid email address"
        },
    ],
    "name": "ZodError"
}
Enter fullscreen mode Exit fullscreen mode

Output (Automatic):

{
  "status": "fail",
  "method": "GET", // showed when development environment,
  "url": "/login", // showed when development environment,
  "message": "Validation error: user.email: Invalid email address;",
  "stack": // showed when development environment,
}
Enter fullscreen mode Exit fullscreen mode

No More try/catch in Express Controllers


With the included asyncHandler, your controllers become clean again.

import { asyncHandler, Errors } from 'ds-express-errors';

const createUser = asyncHandler(async (req, res) => {
  const user = await db.create(req.body); // If this throws, it's handled automatically!
  res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

Graceful Shutdown in Node.js & Express.js 🛑

A graceful shutdown in Express.js ensures your Node.js application can handle critical process-level errors such as uncaughtException and unhandledRejection without immediate crashes.

This approach allows the server to close database connections safely and predictably before the application stops, improving reliability and stability in production.

Implementing a proper Node.js graceful shutdown mechanism helps maintain data integrity and prevents abrupt termination of your Express server.

import { initGlobalHandlers } from 'ds-express-errors';

initGlobalHandlers({
  // Optional: Prevent exit on unhandledRejection (default: true)
  exitOnUnhandledRejection: true,

  // Async callback with error access
  onCrash: async (err) => {
    console.error('CRASH DETECTED:', err.message); // Access the error!

    // Send alert to Sentry/Telegram
    await sendAlertToAdmin(err);

    // Close resources
    await db.disconnect();
    console.log('Cleanup finished.');

    // The library will automatically execute process.exit(1) after this function
  }
});
Enter fullscreen mode Exit fullscreen mode

Configuration (Custom Response Format)

You can customize the structure of the error response sent to the client. This is useful if you need to adhere to a specific API standard (e.g., JSON:API) or hide certain fields.

Customize the dev environment by using devEnvironments: []

Looking to manage an error from a library that isn’t natively supported? Or do you want to modify the standard behavior? You can pass an array of customMappers.

Use setConfig before initializing the error handler middleware.

import { setConfig, errorHandler } from('ds-express-errors');

// Optional: Customize response format
setConfig({
// Your Winston/Pino logger
    customLogger: logger, 
    customMappers: [
        (err, req) => {
            if (err.name === 'newErrorWithReq') {
                return Errors.Forbidden(`[${req.baseUrl}] ${err.message}`)
            }
            //... other if
        }
    ],
    devEnvironments: ['development', 'dev'],
    formatError: (err, {req, isDev}) => {
        return {
            success: false,
            error: {
                code: err.statusCode,
                message: err.message,
                // Add stack trace only in development
                ...(isDev ? { debug_stack: err.stack } : {})
            }
        };
    }
});

const app = express();
// ... your routes ...
app.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

Default Format
If no config is provided, the library uses the default format:

{
  "status": "error", // or 'fail'
  "method": "GET", // showed when development environment
  "url": "/api/resource", // showed when development environment
  "message": "Error description",
  "stack": // showed when development environment,
}
Enter fullscreen mode Exit fullscreen mode

Default Config Format

let config = {
    customMappers: [],
    devEnvironments: ['dev', 'development'],
    formatError: (err, {req, isDev}) => ({ 
        status: err.isOperational ? 'fail' : 'error',
        message: err.message,
        ...(isDev ? { 
            method: req.method,
            url: req.originalUrl,
            stack: err.stack
         } : {})
    })
}
Enter fullscreen mode Exit fullscreen mode

📦 How to use it?

​Installation is standard:

npm install ds-express-errors
Enter fullscreen mode Exit fullscreen mode

📚 Documentation


I've built a full documentation website where you can see advanced usage, API references, and more examples:

👉ds-express-errors.dev

FAQ

1. Does ds-express-errors replace Express’s default error handler?

Yes—it extends it with a fully typed and configurable error-handling layer.

2. Does it work with TypeScript?

Absolutely. The library is built with full TypeScript support.

3. Can I use it with Zod, Joi, Prisma, Mongoose, Sequelize, or JWT?

Yes—the auto-mapping feature automatically converts their native errors into consistent HTTP responses.

Conclusion

This library allowed me to finally unify and simplify Express.js error handling across my Node.js projects. If you want predictable status codes, type-safe errors, and cleaner controllers without endless try/catch blocks, ds-express-errors can save you a lot of time.

Please don't hesitate to try it out and share your feedback. A star on GitHub means a lot. ❤️

Top comments (0)