Enterprise-grade error handling for Node.js
Structured · Sanitized · Rate-limited · Observable
In production environments, try/catch alone is never enough. You need:
- Structured errors — not just strings, but objects with category, level, status code, context, and timestamps
- Sensitive data protection — passwords, tokens, and card numbers must never leak into logs
- Log storm prevention — one failing upstream service can generate millions of identical errors per minute
- Observability — error statistics by category, status code, level, and time dimension
- Standardized error codes — consistent error contracts across microservices
- Express integration — structured JSON error responses with traceId propagation
MaxError solves all of these in a single, zero-external-dependency class (core library). It extends the native Error class, so it works everywhere a regular Error does.
┌─────────────────────────────────────────────────────────────────┐
│ MaxError Instance │
├─────────────┬──────────────┬──────────────┬────────────────────┤
│ Constructor │ handle() │ Recovery │ Error Chain │
│ ─ sanitize │ ─ rate │ ─ retry │ ─ setCause() │
│ ─ aggregate │ limit │ ─ max │ ─ getErrorChain()│
│ ─ stats │ ─ log │ attempts │ │
│ ─ fingerprint│ ─ respond │ │ │
├──────────────┴──────────────┴──────────────┴────────────────────┤
│ Static Services │
├──────────┬───────────┬────────────┬──────────┬─────────────────┤
│ Error │ Rate │ Sanitizer │ Aggreg- │ Express │
│ Codes │ Limiter │ (deep, │ ation │ Middleware │
│ Registry │ (sliding │ recursive)│ (LRU, │ (error handler, │
│ │ window) │ │ dedup) │ traceId) │
├──────────┴───────────┴────────────┴──────────┴─────────────────┤
│ Statistics & Persistence │
│ ─ byCategory / byStatusCode / byLevel / byTime │
│ ─ File or Database storage with debounce │
├────────────────────────────────────────────────────────────────┤
│ Multi-channel Logging │
│ ─ Console (colored) / File (rotation) / Remote (HTTP) │
└────────────────────────────────────────────────────────────────┘
| Feature | Description |
|---|---|
| 🏷️ Error Code System | Register business error codes (AUTH_001, DB_002), create errors with MaxError.fromCode() |
| 🔒 Auto Sanitization | Deep recursive redaction of passwords, tokens, emails, phone numbers, card numbers |
| 🚦 Rate Limiting | Sliding window rate limiter per error fingerprint, prevents log storms |
| 📊 Error Aggregation | Fingerprint-based deduplication with LRU eviction, tracks occurrence count |
| 🔌 Express Middleware | One-line error handler + traceId injection middleware |
| 📈 Statistics | Multi-dimensional stats (category/status/level/time), file or DB persistence |
| 🔄 Recovery | Configurable retry strategies with max attempt limits |
| ⛓️ Error Chaining | Preserve root cause with setCause(), trace with getErrorChain() |
| 🎯 Filtering | Filter errors by category, level, or status code |
| 📝 Multi-channel Logging | Console (colored), file (with rotation), remote HTTP |
| ⏱️ Performance Metrics | Collect operation duration and timing data |
| 🌐 Network Responses | Auto-respond with structured JSON for HTTP errors |
git clone https://github.com/user/max-error.git
cd max-error
pnpm installimport { MaxError, ErrorCategory, ErrorLevel } from './js/MaxError.js';
// Create a structured error
const error = new MaxError(
'User query failed', // custom message
'User not found: u_123', // original message
404, // status code
{ userId: 'u_123' }, // details (auto-sanitized)
{
category: ErrorCategory.DATABASE,
level: ErrorLevel.ERROR,
captureTimestamp: true,
captureStackTrace: true,
captureContext: true,
initialContext: { sessionId: 'sess_abc' },
}
);
// Handle: log + optional HTTP response
error.handle(null, 'quote', { type: 'iso' });Console output:
2025-01-15T08:30:00.000Z|MaxError {
time: '2025-01-15T08:30:00.000Z',
message: 'User query failed',
originalMessage: 'User not found: u_123',
statusCode: 404,
details: { userId: 'u_123' },
category: 'database',
level: 3,
timestamp: 1736930400000,
context: { sessionId: 'sess_abc', timestamp: 1736930400000 },
errorFile: { fileName: 'app.js', lineNumber: 12, columnNumber: 15 }
}
new MaxError(message, originalMessage, statusCode, details, options)| Parameter | Type | Default | Description |
|---|---|---|---|
message |
string |
null |
Custom display message |
originalMessage |
string |
— | Original error message (used as Error.message) |
statusCode |
number |
— | HTTP status code |
details |
object |
{} |
Additional error details (auto-sanitized if enabled) |
options |
object |
{} |
Configuration options (see below) |
| Option | Type | Default | Description |
|---|---|---|---|
category |
string |
'system' |
Error category (from ErrorCategory) |
level |
number |
3 |
Error level (from ErrorLevel) |
captureStackTrace |
boolean |
config | Capture file name, line, column |
captureTimestamp |
boolean |
config | Attach Date.now() timestamp |
captureEnvironment |
boolean |
config | Capture NODE_ENV |
captureContext |
boolean |
config | Enable context object |
captureMetrics |
boolean |
config | Enable metrics collection |
initialContext |
object |
{} |
Initial context key-value pairs |
| Method | Returns | Description |
|---|---|---|
MaxError.fromCode(code, details?, options?) |
MaxError |
Create error from registered error code |
MaxError.registerErrorCode(code, definition) |
void |
Register a single error code |
MaxError.registerErrorCodes(registry) |
void |
Batch register error codes |
MaxError.getAllErrorCodes() |
Map |
Get all registered codes (runtime + config) |
MaxError.getErrorCodeDefinition(code) |
object|null |
Get definition for a specific code |
MaxError.loadConfig(config) |
void |
Merge new configuration |
MaxError.sanitize(data) |
* |
Manually sanitize any data |
MaxError.isRateLimited(fingerprint) |
boolean |
Check if fingerprint is rate-limited |
MaxError.generateFingerprint(error) |
string |
Generate error fingerprint |
MaxError.aggregate(error, fingerprint) |
object |
Record error in aggregation window |
MaxError.getAggregation(fingerprint?) |
object|Map |
Get aggregation stats |
MaxError.expressMiddleware(options?) |
function |
Express error handler middleware |
MaxError.expressRequestContext() |
function |
Express traceId injection middleware |
MaxError.resetStats() |
void |
Clear all stats, rate limit, and aggregation data |
MaxError.saveStats() |
Promise |
Persist stats to file or database |
MaxError.initializeStats(forceRefresh?) |
Promise |
Load stats from storage |
MaxError.cleanup() |
Promise |
Graceful shutdown (save + stop timers) |
| Method | Returns | Description |
|---|---|---|
handle(res?, typeTime, eventTime, options?) |
void |
Log error, optionally send HTTP response |
addContext(key, value) |
this |
Add context key-value pair |
getContext(key) |
* |
Get context value |
setCause(error) |
this |
Set error cause (chaining) |
getErrorChain() |
Array |
Get full cause chain |
collectMetrics(operation, startTime, endTime) |
this |
Record performance metrics |
registerRecovery(fn, maxAttempts?) |
this |
Register retry strategy |
attemptRecovery() |
Promise<boolean> |
Execute one recovery attempt |
matchesFilter(filter) |
boolean |
Check if error matches filter criteria |
getDate(type, eventTime) |
string|number |
Format current time |
| Property | Type | Description |
|---|---|---|
customMessage |
string |
Custom display message |
originalMessage |
string |
Original error message |
statusCode |
number |
HTTP status code |
details |
object |
Error details (sanitized) |
category |
string |
Error category |
level |
number |
Error level |
fingerprint |
string |
Error fingerprint for dedup |
rateLimited |
boolean |
Whether this error was rate-limited |
aggregationCount |
number |
How many times this error has occurred |
isFirstOccurrence |
boolean |
Whether this is the first occurrence |
errorCode |
string |
Business error code (if created via fromCode) |
timestamp |
number |
Creation timestamp (if captured) |
environment |
string |
NODE_ENV value (if captured) |
context |
object |
Context object (if captured) |
metrics |
object |
Performance metrics (if captured) |
fileName |
string |
Source file (if stack captured) |
lineNumber |
number |
Source line (if stack captured) |
columnNumber |
number |
Source column (if stack captured) |
Define and reuse business error codes across your application:
// Register error codes
MaxError.registerErrorCodes({
AUTH_001: { message: 'Not authenticated', statusCode: 401, category: 'authentication', level: 2 },
AUTH_002: { message: 'Token expired', statusCode: 401, category: 'authentication', level: 2 },
DB_001: { message: 'Connection failed', statusCode: 500, category: 'database', level: 4 },
BIZ_001: { message: 'Insufficient funds', statusCode: 400, category: 'business', level: 2 },
});
// Create errors from codes
const error = MaxError.fromCode('AUTH_001', { endpoint: '/api/orders' });
console.log(error.statusCode); // 401
console.log(error.category); // 'authentication'
console.log(error.errorCode); // 'AUTH_001'
// You can also define codes in MaxError.config.js → errorCodes.registry
// They are auto-registered when config loadsAutomatically redacts sensitive fields and patterns before logging:
const error = new MaxError('Payment failed', 'err', 500, {
username: 'john',
password: 'secret123', // → '******'
card: '6222021234567890123', // → '***CARD***'
contact: 'john@example.com', // → '***EMAIL***'
phone: '13812345678', // → '***PHONE***'
nested: {
token: 'abc123', // → '******' (deep recursive)
data: {
ssn: '123-45-6789', // → '******'
}
}
});
// Manual sanitization
const clean = MaxError.sanitize({ password: 'x', email: 'a@b.com' });Sanitizer configuration in MaxError.config.js:
| Option | Type | Description |
|---|---|---|
enabled |
boolean |
Enable/disable auto-sanitization |
fields |
string[] |
Field names to redact (case-insensitive) |
patterns |
Array<{regex, replacement}> |
Regex patterns for value-level redaction |
replacement |
string |
Default replacement text ('******') |
customSanitizer |
function|null |
Custom sanitizer function (data) => sanitizedData |
Sliding window rate limiter prevents log storms from repeated errors:
MaxError.loadConfig({
rateLimiting: {
enabled: true,
windowMs: 60000, // 1 minute window
maxPerWindow: 10, // max 10 per fingerprint per window
onRateLimited: (fingerprint, count) => {
console.warn(`Rate limited: ${fingerprint} (${count} in window)`);
},
},
});
// Errors beyond the limit are automatically skipped in handle()
for (let i = 0; i < 20; i++) {
const err = new MaxError('Same error', 'msg', 500, {});
err.handle(null, 'quote', { type: 'iso' });
// Only the first 10 will actually log
}| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
false |
Enable rate limiting |
windowMs |
number |
60000 |
Sliding window size in ms |
maxPerWindow |
number |
100 |
Max errors per fingerprint per window |
onRateLimited |
function|null |
null |
Callback (fingerprint, count) => {} |
Deduplicates identical errors and tracks occurrence count:
MaxError.loadConfig({
aggregation: {
enabled: true,
windowMs: 60000,
maxFingerprints: 1000, // LRU eviction when exceeded
onAggregated: (fingerprint, count, firstError) => {
if (count % 100 === 0) {
alert(`Error ${fingerprint} occurred ${count} times!`);
}
},
},
});
const err1 = new MaxError('DB timeout', 'timeout', 500, {});
const err2 = new MaxError('DB timeout', 'timeout', 500, {});
console.log(err2.aggregationCount); // 2
console.log(err2.isFirstOccurrence); // false
// Query aggregation data
const agg = MaxError.getAggregation(err1.fingerprint);
console.log(agg.count); // 2One-line integration for Express applications:
import express from 'express';
import { MaxError } from './js/MaxError.js';
const app = express();
// 1. Inject traceId into every request
app.use(MaxError.expressRequestContext());
// 2. Your routes
app.get('/api/users/:id', (req, res) => {
throw MaxError.fromCode('USER_NOT_FOUND', { userId: req.params.id });
});
// 3. Error handler (must be last)
app.use(MaxError.expressMiddleware({
exposeDetails: process.env.NODE_ENV !== 'production',
onError: null, // or custom handler: (error, req, res) => {}
}));
app.listen(3000);Response format:
{
"success": false,
"error": {
"message": "User not found",
"code": "USER_NOT_FOUND",
"category": "database"
}
}With exposeDetails: true (development):
{
"success": false,
"error": {
"message": "User not found",
"code": "USER_NOT_FOUND",
"category": "database",
"details": { "userId": "123" },
"stack": "MaxError: User not found\n at ..."
}
}Register automatic retry logic for recoverable errors:
const error = new MaxError('Service unavailable', 'timeout', 503, {});
error.registerRecovery(async () => {
const response = await fetch('https://api.example.com/health');
if (!response.ok) throw new Error('Still down');
}, 3); // max 3 attempts
let recovered = false;
while (!recovered && error.recovery.attempts < error.recovery.maxAttempts) {
recovered = await error.attemptRecovery();
if (!recovered) {
await new Promise(r => setTimeout(r, 1000)); // wait 1s between retries
}
}
console.log(recovered ? 'Service recovered' : 'Service still down');Preserve the full error cause chain for debugging:
try {
await db.query('SELECT ...');
} catch (dbError) {
const error = new MaxError('Query failed', dbError.message, 500, {
query: 'SELECT ...',
});
error.setCause(dbError);
const chain = error.getErrorChain();
// chain = [MaxError('Query failed'), Error('ECONNREFUSED')]
chain.forEach((err, i) => {
console.log(`${i === 0 ? '→' : ' └─'} ${err.message}`);
});
// → Query failed
// └─ connect ECONNREFUSED 127.0.0.1:3306
}Filter errors by category, level, or status code:
const error = new MaxError('Not found', 'msg', 404, {}, {
category: 'database',
level: 3,
});
error.matchesFilter({ categories: ['database', 'network'] }); // true
error.matchesFilter({ levels: [4] }); // false
error.matchesFilter({ statusCodes: [404, 500] }); // true
error.matchesFilter({ categories: ['system'], levels: [3] }); // falseError stats are updated automatically on every new MaxError():
console.log(MaxError.errorStats);
// {
// total: 42,
// byCategory: { system: 10, network: 20, database: 12 },
// byStatusCode: { '500': 22, '404': 15, '403': 5 },
// byLevel: { '2': 10, '3': 25, '4': 7 },
// byTime: {
// byMinute: { '2025-01-15 08:30': 3, ... },
// byHour: { '2025-01-15 08': 15, ... },
// byDay: { '2025-01-15': 42 }
// }
// }
// Manual save / load
await MaxError.saveStats();
await MaxError.initializeStats(true);
// Reset everything
MaxError.resetStats();
// Graceful shutdown
process.on('SIGTERM', async () => {
await MaxError.cleanup();
process.exit(0);
});Persistence configuration:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Enable persistence |
storageType |
string |
'file' |
'file' or 'database' |
filePath |
string |
'./data/error-stats.json' |
File path for file storage |
interval |
number |
300000 |
Auto-save interval (ms) |
skipUnchanged |
boolean |
true |
Skip save if stats unchanged |
immediateSave |
boolean |
true |
Save on every error (debounced) |
dbConnection |
object|null |
null |
MySQL connection object |
tableName |
string |
'error_stats' |
Database table name |
// Console (default) — colored output
error.handle(null, 'quote', { type: 'iso' });
// File — with automatic rotation
error.handle(null, 'quote', { type: 'iso' }, {
outputType: 'file',
filePath: './logs/error.log',
formatJson: true,
});
// Remote HTTP — with timeout and failure callback
error.handle(null, 'quote', { type: 'iso' }, {
outputType: 'remote',
});Log rotation config:
| Option | Type | Default | Description |
|---|---|---|---|
maxFileSize |
number |
10MB |
Max file size before rotation |
backupCount |
number |
5 |
Number of backup files to keep |
onError |
function |
null |
Custom error callback |
All features are configurable via js/MaxError.config.js or MaxError.loadConfig().
// Runtime config override
MaxError.loadConfig({
rateLimiting: { enabled: true, maxPerWindow: 50 },
sanitizer: { enabled: true },
aggregation: { enabled: true },
});Top-level config sections:
| Section | Description |
|---|---|
categories |
Error category enum definitions |
levels |
Error level enum definitions |
defaultCaptureOptions |
Default capture flags for constructor |
defaultLogConfig |
Default logging output type and format |
networkErrorConfig |
Network error response format |
performance |
Metrics collection and max recovery attempts |
logRetention |
File rotation settings |
remoteLogging |
Remote HTTP logging endpoint and timeout |
defaultContext |
Default context fields |
rateLimiting |
Rate limiter configuration |
sanitizer |
Sensitive data sanitization rules |
aggregation |
Error aggregation settings |
errorCodes |
Error code registry |
middleware |
Express middleware settings |
statsPersistence |
Statistics persistence settings |
max-error/
├── js/
│ ├── MaxError.js # Core library (1650 lines)
│ ├── MaxError.config.js # Full configuration with comments
│ └── MaxError.test.js # 135 unit tests (vitest)
├── examples/
│ ├── 01-basic-usage.js # Constructor, context, metrics
│ ├── 02-error-codes.js # Error code registration & usage
│ ├── 03-sanitizer.js # Sanitization demos
│ ├── 04-rate-limit-and-aggregation.js # Rate limiting & aggregation
│ ├── 05-express-middleware.js # Express integration
│ └── 06-recovery-and-chain.js # Recovery & error chaining
├── data/
│ └── error-stats.json # Persisted error statistics
├── package.json
└── vitest.config.js
node examples/01-basic-usage.js
node examples/02-error-codes.js
node examples/03-sanitizer.js
node examples/04-rate-limit-and-aggregation.js
node examples/06-recovery-and-chain.js
# Express example (requires: pnpm add express)
node examples/05-express-middleware.jspnpm test # Run all 135 tests
pnpm test:watch # Watch modeTest coverage includes: exports, constructor, config validation, statistics, time keys, context/error chain, metrics collection, recovery strategies, error filtering, date formatting, logging, handle method, stack parsing, persistence, rate limiting, sanitization, aggregation, error codes, Express middleware.
| Feature | MaxError | http-errors | boom | verror |
|---|---|---|---|---|
| Error categorization | ✅ | ❌ | ❌ | ❌ |
| Error codes | ✅ | ❌ | ❌ | ❌ |
| Auto sanitization | ✅ | ❌ | ❌ | ❌ |
| Rate limiting | ✅ | ❌ | ❌ | ❌ |
| Error aggregation | ✅ | ❌ | ❌ | ❌ |
| Statistics | ✅ | ❌ | ❌ | ❌ |
| Express middleware | ✅ | ❌ | ✅ | ❌ |
| Error chaining | ✅ | ❌ | ❌ | ✅ |
| Recovery strategies | ✅ | ❌ | ❌ | ❌ |
| Multi-channel logging | ✅ | ❌ | ❌ | ❌ |
| Context tracking | ✅ | ❌ | ❌ | ✅ |
| Performance metrics | ✅ | ❌ | ❌ | ❌ |
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Ensure all tests pass (
pnpm test) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Initial release
- Core error handling with category, level, status code
- Error code system with
registerErrorCode/fromCode - Sensitive data sanitization (field + regex pattern)
- Sliding window rate limiting
- Fingerprint-based error aggregation with LRU eviction
- Express error handler and traceId middleware
- Multi-channel logging (console, file with rotation, remote HTTP)
- Statistics with multi-dimensional tracking and persistence
- Recovery strategies with configurable retry
- Error chaining with
setCause/getErrorChain - 135 unit tests
MIT © MaxKin