A structured error format that preserves context through call stacks and enables serialization for logging, transmission, and debugging.
Standard JavaScript/TypeScript errors lose context 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
- No cause chain: Prior to ES2022, no standard way to chain errors
- Unstructured data: Error messages contain unstructured text mixing metadata
This tool provides a structured error format that:
- Preserves operation context at each level of the call stack
- Serializes to clean JSON for logging and transmission
- Maintains cause chains for root cause analysis
- Separates error codes, categories, and metadata from messages
- Zero dependencies — Uses only Node.js standard library
- Type safe — Full TypeScript support with strict mode
- Composable — Works as both library and CLI
- Serializable — Round-trip JSON serialization/deserialization
- Backwards compatible — Extends standard Error class
Represents metadata about an operation that failed:
interface ErrorContext {
/** The operation that was being performed */
operation: string;
/** Optional component or module name */
component?: string;
/** Additional metadata (must be JSON-serializable) */
metadata?: Record<string, unknown>;
/** ISO 8601 timestamp when context was added */
timestamp: string;
}JSON-serializable representation of an error:
interface SerializedError {
/** Error name/type (e.g., "StructuredError", "TypeError") */
name: string;
/** Human-readable error message */
message: string;
/** Error code for programmatic handling (optional) */
code?: string;
/** Error category for grouping (optional) */
category?: string;
/** Stack trace (optional) */
stack?: string;
/** Context chain, most recent first */
context: ErrorContext[];
/** Serialized cause error (optional) */
cause?: SerializedError;
}Options for creating a StructuredError:
interface StructuredErrorOptions {
/** Error code for programmatic handling */
code?: string;
/** Error category for grouping */
category?: string;
/** The operation being performed */
operation?: string;
/** Component or module name */
component?: string;
/** Additional metadata */
metadata?: Record<string, unknown>;
/** The underlying cause */
cause?: Error;
}{
"name": "StructuredError",
"message": "Failed to process user request",
"code": "USER_NOT_FOUND",
"category": "validation",
"stack": "StructuredError: Failed to process...\n at ...",
"context": [
{
"operation": "handleRequest",
"component": "UserController",
"metadata": { "endpoint": "/api/users/123" },
"timestamp": "2025-12-26T12:00:00.000Z"
},
{
"operation": "fetchUser",
"component": "UserService",
"metadata": { "userId": "123" },
"timestamp": "2025-12-26T11:59:59.500Z"
}
],
"cause": {
"name": "Error",
"message": "User not found in database",
"context": []
}
}Context is stored most recent first:
context[0] = handleRequest (most recent, top of call stack)
context[1] = fetchUser (earlier, one level down)
context[2] = queryDatabase (earliest, bottom of call stack)
This matches the typical reading order when debugging (start from where the error surfaced).
For human-readable output (toString()):
[USER_NOT_FOUND] Failed to process user request
Context:
→ handleRequest (UserController) {"endpoint":"/api/users/123"}
→ fetchUser (UserService) {"userId":"123"}
Caused by:
User not found in database
const error = new StructuredError('Message', {
code: 'ERROR_CODE',
category: 'category',
operation: 'operationName',
component: 'ComponentName',
metadata: { key: 'value' }
});Behavior:
- Sets
nameto"StructuredError" - Sets
messageto provided message - Captures stack trace via
Error.captureStackTrace - If
operationprovided, creates initial context entry with current timestamp
const wrapped = StructuredError.wrap(originalError, 'New message', options);Behavior:
- Creates new StructuredError with provided message
- Sets
causeto the original error - If original was StructuredError:
- Inherits
codeandcategory(unless overridden) - Copies entire context chain
- Inherits
- If operation provided, adds new context entry
const enriched = error.addContext('operation', { component, metadata });Behavior:
- Creates NEW StructuredError instance (immutable pattern)
- Copies all properties from original
- Adds new context entry at position 0 (most recent)
- Preserves original stack trace
const json = error.toJSON(); // Returns SerializedError
const str = JSON.stringify(error); // Uses toJSON()Behavior:
- Serializes all fields to JSON-compatible format
- Recursively serializes cause chain
- Only includes optional fields if they have values
const error = StructuredError.fromJSON(json);Behavior:
- Creates StructuredError from SerializedError
- Restores context chain
- Recursively deserializes cause chain
- Restores stack trace if present
Usage: structured-error-handler <command> [options] [input]
Commands:
demo Show a demo of structured errors
parse <json> Parse JSON error and format it
validate <json> Validate JSON error format
Options:
-f, --format <format> Output format: json, text (default: json)
-s, --stack Include stack traces in output
-h, --help Show this help message
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (invalid input, parse failure) |
const error = new StructuredError('Message'); // No operation
error.context // => []StructuredError.wrap('string error', 'Wrapped');
// cause becomes: new Error('string error')Metadata must be JSON-serializable. Circular references will cause serialization to fail. This is by design - metadata should be simple, serializable data.
No enforced limit on context chain depth. Applications should manage this based on their needs.
- Creation: O(1) - constant time error creation
- addContext: O(n) - copies context array
- toJSON: O(n×m) - n context entries, m cause chain depth
- fromJSON: O(n×m) - same as toJSON
- No sensitive data in metadata: Applications should avoid putting passwords, tokens, or PII in metadata
- Stack traces: May reveal file paths; consider stripping in production logs
- Input validation:
fromJSONtrusts input structure; validate untrusted JSON before parsing - No code execution: Deserialization creates data structures only, no eval or dynamic execution
Uses the standard Error.cause property from ES2022. For older runtimes:
- The
causeproperty is still set and accessible - Some environments may not serialize it by default
- Requires Node.js 18+ (for native test runner)
- Uses
Error.captureStackTracewhen available - Falls back gracefully in environments without it
// Low-level database error
const dbError = new Error('Connection timeout');
// Wrap with database context
const repoError = StructuredError.wrap(dbError, 'Failed to fetch user', {
code: 'DB_TIMEOUT',
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' }
});
console.log(apiError.toString());Output:
[DB_TIMEOUT] Failed to fetch user
Context:
→ handleGetUser (UserController) {"endpoint":"/api/users/123"}
→ getUserProfile (UserService) {"includePreferences":true}
→ findUserById (UserRepository) {"userId":"123","timeout":5000}
Caused by:
Connection timeout
const error = new StructuredError('Invalid email format', {
code: 'VALIDATION_FAILED',
category: 'validation',
operation: 'validateUserInput',
metadata: {
field: 'email',
value: 'not-an-email',
rule: 'email'
}
});
// Programmatic handling
if (error.hasCode('VALIDATION_FAILED')) {
// Return 400 Bad Request
}
if (error.hasCategory('validation')) {
// Log to validation error dashboard
}try {
await processOrder(orderId);
} catch (err) {
const structured = StructuredError.from(err, {
operation: 'processOrder',
metadata: { orderId }
});
// Send to logging service
logger.error({
...structured.toJSON(),
requestId: req.id,
userId: req.user.id
});
// Return appropriate HTTP response
if (structured.hasCategory('validation')) {
res.status(400).json({ error: structured.message });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}- Initial release
- StructuredError class with context preservation
- Serialization/deserialization support
- CLI interface with demo, parse, and validate commands
- Helper functions: serializeError, deserializeError, formatError