r: string;
}
class InputValidationFault extends SystemFault {
constructor(details: Record<string, string>) {
super('Input validation rejected', 'VALIDATION_FAILED', 'info');
this.details = details;
}
public readonly details: Record<string, string>;
}
**Architecture Rationale**: Using an abstract base class enforces consistent metadata across all fault types. The `severity` field enables downstream systems to filter noise (e.g., ignore `info` level validation faults in crash reporting). `Error.captureStackTrace` ensures V8 generates accurate stack traces without duplicating the constructor call in the trace.
### 2. Async Control Flow Wrappers
Asynchronous code breaks traditional control flow. Wrappers standardize how promises are consumed and how failures propagate.
```typescript
type AsyncResult<T> = [data: T, error: null] | [data: null, error: SystemFault];
async function executeAsync<T>(operation: Promise<T>): Promise<AsyncResult<T>> {
try {
const result = await operation;
return [result, null];
} catch (err) {
const fault = err instanceof SystemFault ? err : new SystemFault(String(err), 'UNKNOWN_FAULT', 'critical');
return [null, fault];
}
}
function wrapRoute<T extends (...args: any[]) => Promise<any>>(handler: T) {
return async (...args: Parameters<T>) => {
try {
return await handler(...args);
} catch (err) {
console.error(`Route handler failed: ${handler.name}`, err);
throw err; // Re-throw to let framework middleware handle it
}
};
}
Architecture Rationale: The tuple pattern ([data, error]) eliminates nested try/catch blocks in business logic, making control flow linear and easier to test. The route wrapper isolates handler execution from framework routing, ensuring that unhandled async rejections don't bypass centralized error middleware.
3. Runtime Safety Nets
Both Node.js and browser environments expose global hooks for unhandled failures. These must be configured to preserve process stability and capture telemetry.
// Node.js runtime guards
process.on('uncaughtException', (fault) => {
console.error('CRITICAL: Uncaught exception detected', fault);
// Flush logs, close DB pools, notify ops team
initiateGracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('WARNING: Unhandled promise rejection', reason);
// Do not exit; log and allow event loop to continue
reportToTelemetry({ type: 'unhandledRejection', payload: reason });
});
// Browser runtime guards
window.addEventListener('error', (event) => {
reportToTelemetry({
type: 'runtimeError',
message: event.message,
location: `${event.filename}:${event.lineno}:${event.colno}`,
stack: event.error?.stack
});
});
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault();
reportToTelemetry({ type: 'browserRejection', payload: event.reason });
});
Architecture Rationale: Uncaught exceptions in Node.js leave the V8 engine in an undefined state. The only safe response is graceful shutdown after flushing pending operations. Unhandled rejections, while dangerous, don't immediately corrupt memory; logging them allows teams to identify missing await statements or forgotten .catch() chains without crashing the process.
4. Framework-Specific Routing (Express.js)
Express requires a dedicated middleware signature to intercept errors. This middleware acts as the final routing layer before response serialization.
import { Request, Response, NextFunction } from 'express';
function frameworkErrorHandler(err: any, _req: Request, res: Response, _next: NextFunction) {
const isProduction = process.env.NODE_ENV === 'production';
if (err instanceof SystemFault) {
return res.status(err.severity === 'critical' ? 500 : 400).json({
status: 'error',
code: err.code,
message: err.message,
timestamp: err.timestamp
});
}
if (err.type === 'entity.parse.failed') {
return res.status(400).json({ status: 'error', code: 'MALFORMED_PAYLOAD', message: 'Invalid JSON structure' });
}
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
status: 'error',
code: 'INTERNAL_FAILURE',
message: isProduction ? 'Service temporarily unavailable' : err.message,
...(isProduction ? {} : { stack: err.stack })
});
}
Architecture Rationale: The four-parameter signature (err, req, res, next) is mandatory for Express to recognize this as error-handling middleware. Environment-aware response masking prevents stack trace leakage in production while preserving debugging data in development. Structured JSON responses enable client-side SDKs to parse and display user-friendly messages.
5. Client-Side Isolation (React)
React's rendering cycle requires explicit fault isolation to prevent UI crashes from unmounting the entire application tree.
import React from 'react';
interface ShieldProps {
fallback: React.ReactNode;
children: React.ReactNode;
}
interface ShieldState {
hasFault: boolean;
fault: Error | null;
}
class ComponentShield extends React.Component<ShieldProps, ShieldState> {
state: ShieldState = { hasFault: false, fault: null };
static getDerivedStateFromError(error: Error) {
return { hasFault: true, fault: error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
reportToTelemetry({
type: 'reactRenderFault',
message: error.message,
componentStack: info.componentStack
});
}
render() {
if (this.state.hasFault) {
return this.props.fallback;
}
return this.props.children;
}
}
Architecture Rationale: getDerivedStateFromError synchronously updates state to trigger a fallback render. componentDidCatch runs asynchronously, making it safe for network calls to telemetry services. This separation prevents rendering loops and ensures the UI remains interactive even when child components fail.
Pitfall Guide
1. Swallowing Exceptions in Catch Blocks
Explanation: Catching an error and returning a default value or silently logging it hides failures from monitoring systems and leaves the application in an inconsistent state.
Fix: Always re-throw, return a structured error tuple, or delegate to a centralized handler. If suppression is intentional, explicitly mark it with a comment and increment a counter metric.
2. Assuming Process Stability After Uncaught Exceptions
Explanation: Node.js documentation explicitly states that the runtime is unreliable after an uncaught exception. Continuing execution risks memory corruption, deadlocks, or silent data loss.
Fix: Trigger a graceful shutdown sequence: close database connections, flush log queues, notify orchestration systems, and exit with a non-zero code.
3. Forgetting the Fourth Parameter in Express Middleware
Explanation: Express distinguishes regular middleware from error handlers by the number of parameters. Missing the next parameter causes Express to treat the function as a standard route handler, bypassing error interception.
Fix: Always declare (err, req, res, next) even if next isn't used. Place this middleware after all route definitions.
4. Blocking the Event Loop During Error Reporting
Explanation: Synchronous network calls or heavy serialization inside catch blocks freeze the event loop, causing request timeouts and cascading failures.
Fix: Use asynchronous, non-blocking telemetry calls. Queue error payloads and flush them in batches. Implement circuit breakers to drop reports if the monitoring service is down.
5. Over-Exposing Stack Traces in Production
Explanation: Returning raw stack traces to clients leaks internal implementation details, file paths, and dependency versions, creating security and intellectual property risks.
Fix: Conditionally mask stack traces based on NODE_ENV. Return generic messages to clients while logging full traces internally.
6. Misusing React Error Boundaries with Hooks
Explanation: Error boundaries only work with class components. Functional components using hooks cannot implement getDerivedStateFromError or componentDidCatch.
Fix: Wrap functional components in a class-based boundary, or use libraries like react-error-boundary that provide hook-compatible wrappers.
7. Ignoring Unhandled Promise Rejections
Explanation: Promises that reject without a .catch() or await in a try/catch silently fail. In Node.js, this will crash the process in future versions; in browsers, it pollutes the console and leaks memory.
Fix: Enable process.on('unhandledRejection') in Node.js and window.addEventListener('unhandledrejection') in browsers. Audit codebases for missing error handlers on async operations.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-throughput public API | Centralized middleware + tuple pattern | Standardizes responses, reduces per-request overhead | Low (infrastructure scaling dominates) |
| Internal admin dashboard | Inline try/catch + React boundaries | Simpler codebase, lower monitoring costs | Minimal |
| Real-time collaboration app | Global handlers + async wrappers + circuit breakers | Prevents cascade failures, maintains UI responsiveness | Medium (requires robust telemetry queue) |
| Legacy monolith migration | Gradual boundary wrapping + unhandled rejection tracking | Isolates new code without rewriting entire stack | Low (phased implementation) |
Configuration Template
// fault-manager.config.ts
import { SystemFault, DataRetrievalFailure, InputValidationFault } from './fault-types';
export const faultConfig = {
telemetry: {
endpoint: process.env.TELEMETRY_URL || 'https://logs.internal/api/v1/batch',
flushIntervalMs: 5000,
maxQueueSize: 50,
retryAttempts: 3,
backoffMultiplier: 1.5
},
runtime: {
shutdownTimeoutMs: 10000,
logLevel: process.env.NODE_ENV === 'production' ? 'error' : 'debug',
maskStackTraces: process.env.NODE_ENV === 'production'
},
framework: {
express: {
errorHandlerPath: '/api/v1/errors',
defaultStatusCode: 500,
clientMessage: 'Service temporarily unavailable'
}
}
};
export function classifyFault(err: unknown): SystemFault {
if (err instanceof SystemFault) return err;
if (err instanceof TypeError) return new InputValidationFault({ type: 'type_mismatch' });
return new SystemFault(String(err), 'UNKNOWN_FAULT', 'critical');
}
Quick Start Guide
- Install the base fault class: Copy the
SystemFault abstract class and concrete implementations into your shared utilities directory. Export them for use across services.
- Wire global handlers: Add the Node.js and browser event listeners to your application entry point. Ensure they run before any business logic initializes.
- Replace inline catches: Refactor critical async functions to use
executeAsync() or framework route wrappers. Verify that errors propagate to the centralized middleware.
- Deploy telemetry queue: Integrate the batched reporting configuration. Test with a simulated failure to confirm payloads arrive with enriched context (user ID, route, environment).
- Validate in staging: Trigger controlled failures (invalid payloads, network timeouts, missing resources). Confirm that responses match the structured schema, stack traces are masked in production mode, and monitoring dashboards receive accurate events.