ds-express-errors
v1.9.2
Published
Centralized typed error management library for ExpressJS with Zod/Joi/JWT/express-validator and DB (Prisma/Mongoose/Sequelize) support & graceful shutdown
Maintainers
Readme
📦 DS Express Errors 
DS Express Errors is a library for standardizing error handling in Node.js applications built with Express.
It provides ready-to-use error classes (HTTP Presets), a centralized error handler (middleware), automatic: database error mapping (Mongoose, Prisma, Sequelize), validation error mapping (Zod, Joi, express-validator), JWT and built-in simple logging or custom loggers (Winston/Pino).
Official website & detailed documentation with examples: ds-express-errors
✨ Features
- Ready-to-use HTTP presets:
BadRequest,NotFound,Unauthorized, and others, corresponding to standard HTTP codes. - Centralized handling: One middleware catches all errors and formats them into a unified JSON response.
- Automatic mapping: Converts native errors (like JWT, MongoDB duplicate key errors or Prisma/Sequelize/Zod/Joi validation errors, express-validator) into clear HTTP responses.
- Logging: Built-in logger with levels (
Error,Warning,Info,Debug) and timestamps. - Custom Logger: Easily integrate external loggers like Winston or Pino by passing them into the configuration.
- Security: In production (
NODE_ENV=production), stack traces, sensitive data are hidden; visible in development. - Fully Customizable Response: Adapt the error structure to match your API standards (JSON:API, legacy wrappers, etc.).
- Global Handlers: Optional handling of
uncaughtExceptionandunhandledRejectionwith support for Graceful Shutdown (custom cleanup logic). - TypeScript support: Includes
.d.tsfiles for full typing support.
🚀 Installation
npm install ds-express-errors🛠 Integration
Add errorHandler at the end of your Express middleware chain.
const { errorHandler } = require('ds-express-errors');If you need examples in ESM not commonjs please visit ds-express-errors
[!WARNING]
Good to know
If you use only
errorHandlerthe library would perform only duck-typing checks.
[!TIP]
Want strict checks?
Visit the Configuration section. There you will find the
errorClassesconfig property.
Example:
const express = require('express');
const { errorHandler } = require('ds-express-errors');
const app = express();
// ... your routes ...
// Error handler MUST be after all routes
app.use(errorHandler);
app.listen(3000, () => console.log('Server running...'));[!IMPORTANT]
About Environment and Prod vs Dev Responses
By default, the library relies on the
NODE_ENVenvironment variable to format error responses. To see how Production and Development responses differ, visit the Configuration See thedevEnvironmentsproperty to customize this behavior.
📖 Usage
1. Throwing Errors (Using Presets)
No need to remember status codes. Just import Errors and use the method you need.
const { Errors } = require('ds-express-errors');
app.get('/users/:id', async (req, res, next) => {
const user = await getUserById(req.params.id);
if (!user) {
// Automatically sends 404 with message "User not found"
return next(Errors.NotFound('User not found'));
}
if (!user.isActive) {
// Automatically sends 403
return next(Errors.Forbidden('Access denied'));
}
res.json(user);
});2. Async Function Wrapper (asyncHandler)
Avoid repetitive try/catch in every controller.
const { Errors, asyncHandler } = require('ds-express-errors');
const getUser = asyncHandler(async (req, res, next) => {
const data = await database.query();
if (!data) return next(Errors.BadRequest('No data'));
res.json(data);
});
app.get('/data', getUser);3. Using AppError (Custom Errors)
Create specific errors using the AppError class:
const { AppError } = require('ds-express-errors');
// (message, statusCode, isOperational)
throw new AppError('Custom payment gateway error', 402, true);4. Global Process Handlers (Graceful Shutdown)
You can explicitly enable handling of global errors (uncaughtException, unhandledRejection). This allows you to log the crash and perform cleanup (like closing server connections) before exiting.
Basic Usage:
Logs the error and exits (process.exit(1)).
[!TIP]
More detailed information
Visit the Global Handlers and Graceful Shutdown.
const { initGlobalHandlers } = require('ds-express-errors');
// Initialize at the entry point of your app
initGlobalHandlers();Graceful Shutdown & Global Handlers (v1.8.0+)
DS Express Errors provides a robust way to handle application crashes and termination signals (SIGINT, SIGTERM). It ensures your server stops accepting new connections and finishes active requests before exiting.
initGlobalHandlers(options)
| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| closeServer | Function | undefined | Async function to close your HTTP server. |
| onShutdown | Function | undefined | Cleanup logic (e.g., disconnect DB) during normal exit. |
| onCrash | Function | undefined | Cleanup logic during uncaughtException or unhandledRejection. |
| exitOnUnhandledRejection | Boolean | true | Exit process after rejection. |
| exitOnUncaughtException | Boolean | true | Exit process after exception. |
| maxTimeout | number | 10000 | awaited with a N-second timeout. The library calls process.exit(1) after it completes, so you no longer need to exit manually. |
gracefulHttpClose(server)
A helper that wraps server.close() into a Promise with support for an abort signal.
Example usage
const { initGlobalHandlers, gracefulHttpClose } = require('ds-express-errors');
initGlobalHandlers({
closeServer: gracefulHttpClose(server), // Gracefully close server
onShutdown: async (signal) => {
console.log('Cleaning up...');
await mongoose.disconnect(); // Close DB connections
},
onCrash: async (err, signal) => {
await sendAlertToAdmin(err); // Notify dev team about crash
}
});[!IMPORTANT]
Be aware
initGlobalHandlerscan only be called once. Calling it again will throw aGlobalHandlerAlreadySeterror.
GlobalHandlerAlreadySetis an internal ds-express-errors error with the codeERR_DS_EXPRESS_ERRORS_GLOBAL_HANDLER_ALREADY_SET.
📋 Available Error Presets
All methods are available via the Errors object. Default isOperational is true.
| Method | Status Code | Description |
|--------|------------|-------------|
| Errors.BadRequest(message) | 400 | Bad Request |
| Errors.Unauthorized(message) | 401 | Unauthorized |
| Errors.PaymentRequired(message) | 402 | Payment Required |
| Errors.Forbidden(message) | 403 | Forbidden |
| Errors.NotFound(message) | 404 | Not Found |
| Errors.Conflict(message) | 409 | Conflict |
| Errors.UnprocessableContent(message) | 422 | Unprocessable Content |
| Errors.TooManyRequests(message) | 429 | Too Many Requests |
| Errors.InternalServerError(message) | 500 | Internal Server Error |
| Errors.NotImplemented(message) | 501 | Not Implemented |
| Errors.BadGateway(message) | 502 | Bad Gateway |
| Errors.ServiceUnavailable(message) | 503 | Service Unavailable |
| Errors.GatewayTimeout(message) | 504 | Gateway Timeout |
[!TIP]
More detailed information
Visit the API REFERENCES.
⚙️ Configuration & Environment Variables
NODE_ENV:development— stack trace included in responseproduction(or any other) — stack trace hidden, onlymessageandstatusreturned
You can define your own dev environment name using
setConfig
[!IMPORTANT]
About Environment and Prod vs Dev Responses
If
NODE_ENVis not defined, you will receive sanitised error messages, as the library behaves as if the environment were set to production. To see how Production and Development responses differ, visit the Configuration See thedevEnvironmentsproperty to customize which environments should display full error information (stack traces and full error messages).
⚙️ Configuration
DEBUG=true— outputs extra debug info about error mapping (mapErrorNameToPreset)
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.
Also you can customize dev environment by using devEnvironments: []
Use setConfig before initializing the error handler middleware.
[!IMPORTANT]
Be aware
setConfigcan only be called once. Calling it again will throw aConfigAlreadySeterror.
ConfigAlreadySetis an internal ds-express-errors error with the codeERR_DS_EXPRESS_ERRORS_CONFIG_ALREADY_SET.
[!IMPORTANT]
customMappersmust be synchronous. Async function or Promise are not supported and will be ignored.
const { setConfig, errorHandler } = require('ds-express-errors');
const logger = require('./utils/logger'); // Your Winston/Pino logger
const z = require('zod');
const Joi = require('joi');
const { Sequelize } = require('sequelize');
const { PrismaClient, Prisma } = require('@prisma/client');
// Optional: Customize response format and Logger
setConfig({
// (OPTIONAL)
customLogger: logger,
// From version v1.8.0+
// (OPTIONAL) You can replace default ds-express-errors check (duck-typing) to more strict by passing error class
// For now is available only Zod, Joi, Sequelize, Prisma
errorClasses: {
Zod: z,
Joi: Joi,
Prisma,
Sequelize
},
// (OPTIONAL) By default ds-express-errors use all available mapper, but from v1.8.0+ you can choose only needed mappers
// Mappers ['zod', 'joi', 'mongoose', 'prisma', 'sequelize', 'expressValidator']
needMappers: ['zod', 'joi', 'prisma'], // (In this example) For now library would map only ['zod', 'joi', 'prisma'] errors, other would be `InternalServerError` or if is specified `customMappers` it would use that response
// ----
// (OPTIONAL) Set preferred log rate per 1 minute
maxLoggerRequests: 1000,
// (OPTIONAL) Define your custom mappers and ds-express-errors would use them first
customMappers: [
(err) => {
if (err.name === 'newError') {
return Errors.BadRequest()
}
}
// ...
],
// (OPTIONAL)
devEnvironments: ['development', 'dev'],
// (OPTIONAL)
formatError: (err, {req, isDev}) => {
return {
success: false,
error: {
code: err.statusCode,
message: err.message,
...(isDev ? { debug_stack: err.stack } : {})
}
};
}
});
const app = express();
// ... your routes ...
app.use(errorHandler);🔌 Library logger
By default if you not set customLogger in setConfig library used his own logger
Library logger have rate logging limits support
| Loggers | params | Output color | |------------|-------------------------|---------------| | logError | error, req (optional) | red | | logWarning | message, req (optional) | yellow | | logInfo | message | default white | | logDebug | message, req (optional) | blue |
🔌 Custom Logger
You can connect your own logger (like Winston, Pino) instead of the built-in console logger.
The object must support 4 methods: error, warn, info, debug.
const { setConfig } = require('ds-express-errors');
const winston = require('winston'); // Example
const logger = winston.createLogger({
// ... your winston config
});
// Pass your logger instance
setConfig({
customLogger: logger
});Default Response Format
If no config is provided, the library uses the default format:
{
"status": "error", // or 'fail'
"method": "GET", // showed when NODE_ENV= development or dev
"url": "/api/resource", // showed when NODE_ENV= development or dev
"message": "Error description",
"stack": // showed when NODE_ENV= development or dev
}[!TIP]
Want a custom response format?
Define the
formatErrorproperty insetConfig. More in Configuration
Default Config Format
let config = {
customMappers: [],
customLogger: null,
errorClasses: null,
needMappers: null,
maxLoggerRequests: 100,
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
} : {})
})
}🛡 Third-Party Error Mapping
mapErrorNameToPreset automatically maps non-AppError instances (e.g., database errors) to HTTP responses.
Supported mappings:
- JWT:
JsonWebTokenError,TokenExpiredError,NotBeforeError→ mapped to401 Unauthorized - express-validator: (v1.7.0+)
FieldValidationError,GroupedAlternativeValidationError,AlternativeValidationError→ mapped to422 Unprocessable ContentandUnknownFieldsError→ mapped to400 Bad Request - Validation Libraries:
ZodError(Zod),ValidationError(Joi) — automatically formatted into readable messages. - Mongoose / MongoDB:
CastError,DuplicateKeyError(code 11000),ValidationError,MongoServerErroris handled (400 for bad JSON body, 500 for code errors, 409 colflict). - Prisma:
PrismaClientKnownRequestError,PrismaClientUnknownRequestError,PrismaClientRustPanicError,PrismaClientInitializationError,PrismaClientValidationError - Sequelize:
SequelizeUniqueConstraintError,SequelizeValidationError,SequelizeForeignKeyConstraintError,SequelizeOptimisticLockError,SequelizeEmptyResultError,SequelizeDatabaseError,SequelizeConnectionError,SequelizeTimeoutError,SequelizeConnectionRefusedError,SequelizeHostNotFoundError,SequelizeHostNotReachableError,SequelizeAccessDeniedError - JS Native:
ReferenceError,TypeError→ mapped to500.SyntaxErroris handled (400 for bad JSON body, 500 for code errors).
Supported Prisma Error Codes:
| Error Code | Dev Message | Prod Message | HTTP Status | | ---------- | ----------------------------- | --------------------- | ----------- | | P2000 | Value too long for column: ... | Invalid input value | 400 | | P2001 | Record does not exist: ... | Resource not found | 404 | | P2002 | Unique constraint failed: ... | Resource already exists | 409 | | P2003 | Foreign key constraint failed: ... | Invalid reference | 400 | | P2005 | The value stored in the database for the field is invalid for the field's type: ... | Invalid data | 400 | | P2006 | The provided value for the field is not valid: ... | Invalid input value | 400 | | P2007 | Data validation error: ... | Invalid reference | 400 | | P2011 | Null constraint violation: ... | Required value is missing | 400 | | P2014 | Required relation violation: ... | Invalid relation | 400 | | P2015 | A related record could not be found: ... | Requested resource not found | 404 | | P2021 | Table does not exist: ... | Internal server error | 500 | | P2022 | Column does not exist: ... | Internal server error | 500 | | P2025 | Record not found: ... | Resource not found | 404 | | P2027 | Multiple errors occurred on the database during query execution: ... | Internal server error | 500 | | P1001 | Cannot reach database: ... | Service unavailable | 503 | | P1002 | Database timeout: ... | Service unavailable | 503 | | P1003 | Database does not exist: ... | Internal server error | 500
[!NOTE]
Example Prisma output for dev:
[2026-07-23T12:01:34.442Z] POST /prisma/p2003 MESSAGE: Prisma P2003: [PrismaClientKnownRequestError] Foreign key constraint failed: { modelName: Post }; { field_name: Post_authorId_fkey (index) } Operation: `prisma.post.create()` StatusCode: 400 Stack: Error: Prisma P2003: [PrismaClientKnownRequestError] Foreign key constraint failed: { modelName: Post }; { field_name: Post_authorId_fkey (index) } Operation: `prisma.post.create()` at BadRequest C:\... Operational: true
Supported Sequelize errors:
| Error Code / Type | Prod Message | HTTP Status | |------------------|-------------|-------------| | SequelizeValidationError | email must be unique | 400 | | SequelizeUniqueConstraintError | Resource already exists | 409 | | SequelizeForeignKeyConstraintError | Invalid reference | 409 | | SequelizeOptimisticLockError | Resource conflict occurred | 409 | | SequelizeEmptyResultError | Resource not found | 404 | | SequelizeDatabaseError | Database error occurred | 500 | | SequelizeConnectionError | Database connection error occurred | 503 | | SequelizeTimeoutError | Database timeout error occurred | 504 | | SequelizeConnectionRefusedError | Database connection error occurred | 503 | | SequelizeHostNotFoundError | Database connection error occurred | 503 | | SequelizeHostNotReachableError | Database connection error occurred | 503 | | SequelizeAccessDeniedError | Database connection error occurred | 503 |
[!NOTE]
Example Sequelize output for dev:
[2026-07-23T13:02:44.783Z] POST /sequelize/unique-constraint MESSAGE: Sequelize: [UniqueConstraintError]: sku must be unique StatusCode: 409 Stack: Error: Sequelize: [UniqueConstraintError]: sku must be unique at Conflict C:... Operational: true
📝 Example Client Response
Development mode:
{
"status": "error",
"method": "GET",
"url": "/api/users/999",
"message": "User not found",
"stack": "Error: User not found\n at /app/controllers/user.js:15:20..."
}Production mode:
{
"status": "error",
"message": "User not found"
}
