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
- 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.
npm install ds-express-errorsAdd 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
If you use only errorHandler the library would perform only duck-typing checks.
Tip
Visit the Configuration section. There you will find the errorClasses config 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
By default, the library relies on the NODE_ENV environment variable to format error responses.
To see how Production and Development responses differ, visit the Configuration
See the devEnvironments property to customize this behavior.
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);
});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);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);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)).
const { initGlobalHandlers } = require('ds-express-errors');
// Initialize at the entry point of your app
initGlobalHandlers();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.
| 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. |
A helper that wraps server.close() into a Promise with support for an abort signal.
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
initGlobalHandlers can only be called once. Calling it again will throw a GlobalHandlerAlreadySet error.
GlobalHandlerAlreadySet is an internal ds-express-errors error with the code ERR_DS_EXPRESS_ERRORS_GLOBAL_HANDLER_ALREADY_SET.
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 |
-
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
If NODE_ENV is 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 the devEnvironments property to customize which environments should display full error information (stack traces and full error messages).
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
setConfig can only be called once. Calling it again will throw a ConfigAlreadySet error.
ConfigAlreadySet is an internal ds-express-errors error with the code ERR_DS_EXPRESS_ERRORS_CONFIG_ALREADY_SET.
Important
customMappers must 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);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 |
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
Define the formatError property in setConfig.
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
} : {})
})
}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).
| 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
[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
| 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
[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
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"
}