Structured error format with context preservation and serialization for debugging, logging, and error transmission.
Standard JavaScript/TypeScript errors lose critical information when they propagate through call stacks:
- Context loss: When re-throwing errors, "what operation failed" is lost
- Serialization issues: Error objects don't serialize cleanly to JSON for logging
- No cause chain: Wrapping errors loses the original error information
- Unstructured data: Error messages mix metadata with human-readable text
This tool provides a structured error format that:
- Preserves operation context at each level of the call stack
- Serializes to clean JSON for logging, transmission, and debugging
- Maintains cause chains for root cause analysis
- Separates error codes, categories, and metadata from messages
- Zero runtime dependencies
- Works with Node.js 18+
- TypeScript support with strict mode
- Composable via CLI or library API
- Full JSON serialization/deserialization round-trip
- Context chain preservation through error wrapping
- Error codes and categories for programmatic handling
- Immutable error enrichment via
addContext()
Clone the repository:
git clone https://github.com/tuulbelt/structured-error-handler.git
cd structured-error-handler
npm install # Install dev dependencies onlyCLI names - both short and long forms work:
- Short (recommended):
serr - Long:
structured-error-handler
Recommended setup - install globally for easy access:
npm link # Enable the 'serr' command globally
serr --helpNo runtime dependencies — this tool uses only Node.js standard library.
import { StructuredError, serializeError, formatError } from './src/index.js';
// Create a new structured error
const error = new StructuredError('Failed to process request', {
code: 'VALIDATION_FAILED',
category: 'validation',
operation: 'validateInput',
component: 'RequestHandler',
metadata: { field: 'email', value: 'not-an-email' }
});
// Check error properties programmatically
if (error.hasCode('VALIDATION_FAILED')) {
// Return 400 Bad Request
}
if (error.hasCategory('validation')) {
// Log to validation dashboard
}
// Serialize for logging
console.log(JSON.stringify(error.toJSON(), null, 2));import { StructuredError } from './src/index.js';
async function fetchUserProfile(userId: string) {
try {
const user = await database.findUser(userId);
return user;
} catch (err) {
throw StructuredError.wrap(err, 'Failed to fetch user profile', {
code: 'USER_FETCH_FAILED',
category: 'database',
operation: 'fetchUserProfile',
component: 'UserService',
metadata: { userId }
});
}
}// Add context as errors propagate up the call stack
const enrichedError = error.addContext('handleRequest', {
component: 'APIController',
metadata: { endpoint: '/api/users/123' }
});
// Context is stored most recent first:
// enrichedError.context[0] = handleRequest (most recent)
// enrichedError.context[1] = fetchUserProfile (earlier)# Show a demo of structured errors
serr demo
# Demo with text format
serr demo --format text
# Parse and format a JSON error
serr parse '{"message":"test error","code":"TEST","context":[]}'
# Validate JSON error format
serr validate '{"message":"test"}'
# Show help
serr --helpdemo— Show a demo of structured errors with context chainingparse <json>— Parse a JSON error and format itvalidate <json>— Validate JSON error format-f, --format <format>— Output format:jsonortext(default:json)-s, --stack— Include stack traces in output-h, --help— Show help message
{
"name": "StructuredError",
"message": "Failed to fetch user profile",
"code": "USER_FETCH_FAILED",
"category": "database",
"context": [
{
"operation": "handleRequest",
"component": "APIController",
"metadata": { "endpoint": "/api/users/123" },
"timestamp": "2025-12-26T12:00:00.000Z"
},
{
"operation": "fetchUserProfile",
"component": "UserService",
"metadata": { "userId": "123" },
"timestamp": "2025-12-26T11:59:59.500Z"
}
],
"cause": {
"name": "Error",
"message": "Connection refused",
"context": []
}
}[USER_FETCH_FAILED] Failed to fetch user profile
Context:
→ handleRequest (APIController) {"endpoint":"/api/users/123"}
→ fetchUserProfile (UserService) {"userId":"123"}
Caused by:
Connection refused
new StructuredError(message: string, options?: StructuredErrorOptions)Options:
code— Error code for programmatic handling (e.g.,'ENOENT','VALIDATION_FAILED')category— Error category for grouping (e.g.,'io','validation','network')operation— The operation being performed when error occurredcomponent— Component or module namemetadata— Additional metadata (must be JSON-serializable)cause— The underlying cause (another error)
| Method | Description |
|---|---|
addContext(operation, options?) |
Add context to error (returns new StructuredError) |
toJSON() |
Serialize to JSON-compatible object |
toString() |
Format for human-readable output |
getRootCause() |
Get the deepest error in the cause chain |
getCauseChain() |
Get all errors in the cause chain as array |
hasCode(code) |
Check if error or any cause matches code |
hasCategory(category) |
Check if error or any cause matches category |
| Method | Description |
|---|---|
StructuredError.wrap(error, message, options?) |
Wrap an existing error with context |
StructuredError.from(error, options?) |
Convert any error to StructuredError |
StructuredError.fromJSON(json) |
Deserialize from JSON |
StructuredError.isStructuredError(error) |
Type guard for StructuredError |
// Serialize any error to JSON format
serializeError(error: Error): SerializedError
// Deserialize back to Error/StructuredError
deserializeError(json: SerializedError): Error
// Format error for human-readable output
formatError(error: Error, options?: { includeStack?: boolean }): stringSee the examples/ directory for runnable examples:
npx tsx examples/basic.ts # Basic usage
npx tsx examples/advanced.ts # Context chaining and cause analysis// Low-level database error
const dbError = new Error('Connection refused');
// Wrap with database context
const repoError = StructuredError.wrap(dbError, 'Failed to fetch user', {
code: 'DB_ERROR',
category: 'database',
operation: 'findUserById',
component: 'UserRepository',
metadata: { userId: '123', timeout: 5000 }
});
// Wrap with service context
const serviceError = repoError.addContext('getUserProfile', {
component: 'UserService',
metadata: { includePreferences: true }
});
// Wrap with API context
const apiError = serviceError.addContext('handleGetUser', {
component: 'UserController',
metadata: { endpoint: '/api/users/123' }
});
// Serialize for logging
logger.error(apiError.toJSON());try {
await processOrder(orderId);
} catch (err) {
const structured = StructuredError.from(err, {
operation: 'processOrder',
metadata: { orderId }
});
// Route based on error type
if (structured.hasCategory('validation')) {
res.status(400).json({ error: structured.message });
} else if (structured.hasCategory('authorization')) {
res.status(403).json({ error: 'Access denied' });
} else {
res.status(500).json({ error: 'Internal server error' });
}
// Log full context
logger.error(structured.toJSON());
}Always use toSafeJSON() in production:
- Excludes stack traces (prevents path disclosure)
- Sanitizes sensitive metadata keys (password, secret, token, api_key, auth, credential, private, key)
// Production (safe)
console.log(error.toSafeJSON());
console.log(error.toSafeJSON({ sanitizeMetadata: true })); // Also redacts sensitive keys
// Development only (includes stack traces)
console.log(error.toJSON());- Use
toSafeJSON({ sanitizeMetadata: true })when context may contain secrets - Stack traces are always excluded from safe serialization
- Original error objects maintain full information for debugging
npm test # Run all tests
npm test -- --watch # Watch modeThe test suite includes:
- Constructor and property tests
- Context chaining tests
- Error wrapping tests
- Serialization/deserialization round-trip tests
- Helper function tests
- CLI command tests
- Edge case handling tests
0— Success1— Error (invalid input, parse failure, unknown command)
Errors are structured and returned via the library API, not thrown. CLI errors are printed to stderr with appropriate exit codes.
See SPEC.md for detailed technical specification including:
- Core type definitions
- Serialization format
- Context chain ordering
- Behavior specifications
- Edge cases
Run flakiness detection to validate test reliability:
./scripts/dogfood-flaky.sh 10Output Diffing Utility - Verify consistent serialization:
./scripts/dogfood-diff.sh
# Compares serialized error outputs to ensure consistencySee DOGFOODING_STRATEGY.md for implementation details.
Potential improvements for future versions:
- Stack trace parsing for better cause chain formatting
- Integration with popular logging libraries (pino, winston)
- Custom serializers for non-JSON-serializable metadata
- Error aggregation for batch operations
▶ View interactive recording on asciinema.org
MIT — see LICENSE
See CONTRIBUTING.md for contribution guidelines.
Part of the Tuulbelt collection:
- Test Flakiness Detector — Detect unreliable tests
- CLI Progress Reporting — Concurrent-safe progress updates
- Cross-Platform Path Normalizer — Path consistency
- File-Based Semaphore — Cross-platform process locking
- Output Diffing Utility — Semantic diff for JSON, text, binary
