const diagnostic = new DiagnosticError(`Failed: ${label}`, {
originalError: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
console.error(diagnostic.toJSON());
throw diagnostic;
});
}
**Architecture Rationale:** Typed errors preserve stack traces across async boundaries. The `context` payload attaches request IDs, user states, or configuration snapshots without polluting the error message. `Error.captureStackTrace` ensures V8 optimizes stack generation. This pattern replaces silent failures with auditable diagnostics.
### Phase 2: Performance Instrumentation
`console.time` is convenient but lacks programmatic control and production safety. The Performance API provides measurable, exportable metrics that integrate with monitoring pipelines.
```typescript
class PerformanceTracker {
private marks: Map<string, number> = new Map();
start(label: string): void {
this.marks.set(label, performance.now());
performance.mark(`${label}-start`);
}
end(label: string): number {
const start = this.marks.get(label);
if (start === undefined) {
throw new DiagnosticError('Performance mark not found', { label });
}
const duration = performance.now() - start;
performance.mark(`${label}-end`);
performance.measure(label, `${label}-start`, `${label}-end`);
this.marks.delete(label);
return duration;
}
getEntries(): PerformanceEntryList {
return performance.getEntriesByType('measure');
}
}
const tracker = new PerformanceTracker();
tracker.start('dataTransformation');
const transformed = heavyData.map(processItem);
const elapsed = tracker.end('dataTransformation');
if (elapsed > 500) {
console.warn(`Performance threshold exceeded: ${elapsed}ms`);
}
Architecture Rationale: performance.mark and performance.measure generate browser-native entries visible in DevTools Performance tab. The class encapsulates state, prevents mark collisions, and returns numeric durations for alerting. Threshold checks replace manual timing logs with automated performance budgets.
Phase 3: Network Request Diagnostics
Frontend-backend communication failures often masquerade as UI bugs. A typed interceptor captures request payloads, response headers, and HTTP status codes before they reach application logic.
interface RequestConfig {
url: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: unknown;
}
interface NetworkDiagnostic {
status: number;
statusText: string;
duration: number;
requestHeaders: Record<string, string>;
responseHeaders: Record<string, string>;
responseBody: unknown;
}
async function fetchWithDiagnostics(config: RequestConfig): Promise<NetworkDiagnostic> {
const start = performance.now();
const response = await fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.body ? JSON.stringify(config.body) : undefined,
});
const duration = performance.now() - start;
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
const responseBody = await response.json().catch(() => null);
if (!response.ok) {
throw new DiagnosticError(`HTTP ${response.status}`, {
url: config.url,
method: config.method,
duration,
responseBody,
});
}
return {
status: response.status,
statusText: response.statusText,
duration,
requestHeaders: config.headers ?? {},
responseHeaders,
responseBody,
};
}
Architecture Rationale: Decoupling network diagnostics from business logic prevents UI components from handling HTTP state. The interceptor normalizes headers, captures timing, and throws typed errors on non-2xx responses. This pattern enables centralized retry logic, circuit breakers, and error tracking integration.
Phase 4: Memory-Safe Event Management
Memory leaks in JavaScript rarely stem from missing delete statements. They originate from retained references in closures, unremoved event listeners, and stale DOM handles. Modern cleanup relies on AbortController and WeakRef.
class EventBus {
private listeners: Map<string, Set<EventListener>> = new Map();
private controllers: Map<string, AbortController> = new Map();
on(event: string, handler: EventListener): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
}
attachToElement(element: HTMLElement, event: string, handler: EventListener): void {
const controller = new AbortController();
element.addEventListener(event, handler, { signal: controller.signal });
this.controllers.set(`${event}-${element.id}`, controller);
}
cleanup(eventKey: string): void {
const controller = this.controllers.get(eventKey);
if (controller) {
controller.abort();
this.controllers.delete(eventKey);
}
}
destroy(): void {
this.controllers.forEach((ctrl) => ctrl.abort());
this.controllers.clear();
this.listeners.clear();
}
}
// WeakRef for non-blocking caches
const elementCache = new WeakMap<HTMLElement, unknown>();
function cacheHeavyData(el: HTMLElement, data: unknown): void {
elementCache.set(el, data);
}
Architecture Rationale: AbortController provides a single cancellation token for multiple listeners, eliminating manual removeEventListener calls. WeakMap ensures cached data is garbage-collected when the DOM node is detached. This pattern prevents the most common frontend memory leaks without requiring manual cleanup tracking.
Pitfall Guide
1. Silent Error Swallowing
Explanation: Empty catch blocks or catch (e) { } suppress stack traces and prevent error tracking services from capturing failures. The application continues in an undefined state.
Fix: Always log or rethrow. Use typed error classes that preserve cause chains. Integrate with Sentry, Rollbar, or Datadog for production visibility.
2. Console Log Sprawl
Explanation: Unstructured console.log statements degrade runtime performance, clutter DevTools, and make it impossible to distinguish diagnostic output from application logs.
Fix: Replace with structured logging libraries (pino, winston) or use console.group/console.table for scoped output. Remove or disable logs in production builds via environment flags.
3. Ignoring Closure Scope During Inspection
Explanation: Breakpoints pause execution, but developers frequently overlook the Scope panel. Variables captured by closures retain references that appear undefined in the Local scope.
Fix: Always inspect Local, Closure, and Block scopes when paused. Use the Watch panel to track closure variables across async boundaries.
4. Missing Conditional Breakpoints
Explanation: Pausing on every iteration of a loop or every API call floods the debugger and obscures the actual failure condition.
Fix: Right-click breakpoints in DevTools β Edit Breakpoint β enter conditions like userId === 'target-id' or response.status >= 400. This isolates specific execution paths without modifying code.
5. Neglecting Source Maps in Production
Explanation: Minified and transpiled code strips line numbers and variable names. Error reports point to unreadable bundle files, making stack traces useless.
Fix: Configure build tools to generate .map files. Use hidden-source-map in Webpack to prevent public exposure while retaining maps for error tracking services. Upload maps to Sentry during CI/CD.
6. Assuming Network Success Without Validation
Explanation: fetch only rejects on network failures, not HTTP errors. Developers frequently parse JSON without checking response.ok, leading to unhandled exceptions on 4xx/5xx responses.
Fix: Always validate response.ok before parsing. Throw typed errors with status codes and response bodies. Implement retry logic with exponential backoff for transient failures.
7. Memory Retention via Stale DOM References
Explanation: Removing an element from the DOM does not free JavaScript references. Variables holding HTMLElement objects prevent garbage collection, causing gradual memory bloat.
Fix: Nullify references after cleanup. Use WeakRef for caches. Prefer AbortController for event listeners. Run heap snapshots before and after user actions to detect retained objects.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Local development debugging | Conditional breakpoints + Watch panel | Fast iteration, no code changes required | Zero |
| Staging performance regression | performance.measure + DevTools flame chart | Identifies exact function bottlenecks | Low (instrumentation overhead) |
| Production error tracking | Typed error classes + Sentry/Rollbar upload | Preserves stack traces across minification | Medium (SaaS subscription) |
| Memory leak investigation | Heap snapshots + WeakRef/AbortController | Isolates retained objects and prevents future leaks | Low (refactoring time) |
| Network failure analysis | Fetch interceptor + status validation | Separates HTTP state from UI logic | Low (wrapper implementation) |
Configuration Template
// diagnostics.config.ts
import { DiagnosticError } from './DiagnosticError';
import { PerformanceTracker } from './PerformanceTracker';
import { fetchWithDiagnostics } from './NetworkDiagnostics';
import { EventBus } from './EventBus';
export const diagnosticsConfig = {
errorHandling: {
captureStack: true,
attachContext: true,
maxContextSize: 1024, // bytes
},
performance: {
thresholds: {
render: 16, // ms (60fps budget)
network: 500, // ms
transformation: 300, // ms
},
exportInterval: 30000, // ms
},
network: {
retries: 3,
backoffMultiplier: 2,
maxBackoff: 5000,
},
memory: {
cleanupOnRouteChange: true,
weakRefCacheEnabled: true,
},
};
// Initialize singletons
export const perfTracker = new PerformanceTracker();
export const eventBus = new EventBus();
// Global error handler
window.addEventListener('unhandledrejection', (event) => {
const err = event.reason;
if (err instanceof Error) {
console.error(new DiagnosticError('Unhandled Promise Rejection', {
message: err.message,
stack: err.stack,
}).toJSON());
}
});
Quick Start Guide
- Install diagnostic utilities: Copy
DiagnosticError, PerformanceTracker, fetchWithDiagnostics, and EventBus into your project's utils/diagnostics/ directory.
- Configure build tooling: Add
devtool: 'hidden-source-map' to your Webpack/Vite config. Set up a post-build script to upload .map files to your error tracking service.
- Replace error handling: Search for
catch blocks and empty handlers. Replace them with executeWithDiagnostics or typed error throws.
- Instrument hot paths: Wrap critical functions with
perfTracker.start()/perfTracker.end() and define threshold alerts.
- Validate in DevTools: Open Chrome DevTools β Performance tab β record a user flow. Verify that marks appear, breakpoints respect conditions, and heap snapshots show stable memory curves.