tRecord {
id: string;
amount: number;
currency: string;
status: 'pending' | 'completed' | 'failed';
}
class TransactionValidator {
async validateBatch(records: PaymentRecord[]): Promise<PaymentRecord[]> {
const processed: PaymentRecord[] = [];
for (const record of records) {
// Execution halts here in DevTools. Inspect `record`, step forward, or evaluate expressions.
debugger;
const sanitized = {
...record,
amount: Number(record.amount.toFixed(2)),
currency: record.currency.toUpperCase(),
};
if (sanitized.amount <= 0) {
sanitized.status = 'failed';
}
processed.push(sanitized);
}
return processed;
}
}
**Why this works:** The `debugger` keyword is part of the ECMAScript specification. When DevTools or an attached inspector is active, execution pauses exactly at that line. You gain access to the call stack, local variables, closure scope, and the ability to step through instructions line-by-line. Unlike logging, you can modify variable values mid-execution to test edge cases without restarting the application.
### Step 2: Non-Intrusive Observation with Logpoints
Logpoints eliminate the need to modify source files for temporary observation. They are configured directly in the inspector and output to the console without pausing execution.
```typescript
class DataSyncEngine {
async syncRecords(endpoint: string, payload: Record<string, unknown>): Promise<void> {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
// In DevTools: Right-click this line β Add logpoint β `Sync status: ${response.status}`
if (!response.ok) {
throw new Error(`Sync failed: ${response.statusText}`);
}
}
}
Why this works: Logpoints are evaluated at runtime but never compiled into the bundle. They support template literals, variable interpolation, and conditional logic. This is critical for debugging high-frequency loops or event handlers where pausing execution would freeze the UI or drop events.
Step 3: Advanced Console API for Structured Output
When logging is unavoidable, replace console.log with structured methods that preserve context and improve readability.
class AuditLogger {
static trackOperation(operation: string, metadata: Record<string, unknown>): void {
console.groupCollapsed(`π ${operation}`);
console.table(metadata);
console.time('operation-duration');
// Simulate async work
setTimeout(() => {
console.timeEnd('operation-duration');
console.groupEnd();
}, 120);
}
static assertCondition(condition: boolean, message: string): void {
console.assert(condition, {
timestamp: new Date().toISOString(),
failure: message,
stack: new Error().stack,
});
}
}
Why this works: console.groupCollapsed keeps the console clean until expanded. console.table renders objects and arrays in a structured grid. console.time/timeEnd provides millisecond-accurate duration tracking without manual timestamp math. console.assert logs only when conditions fail, reducing noise during normal execution.
Step 4: Runtime Environment & Production Telemetry
Local debugging only covers development scenarios. Production failures require source maps, error tracking, and session correlation.
// Source map verification utility
function verifySourceMapIntegrity(): void {
const script = document.querySelector('script[src*="bundle"]');
if (script) {
console.info('Source map endpoint:', script.getAttribute('src')?.replace('.js', '.js.map'));
}
}
// Production error correlation
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
});
function captureDiagnosticError(error: Error, context: Record<string, unknown>): void {
Sentry.captureException(error, {
extra: context,
tags: { module: 'diagnostics', severity: 'high' },
});
}
Why this works: Source maps bridge the gap between minified production code and original source files. Sentry captures stack traces, browser metadata, and custom context, enabling deterministic reproduction of production failures. Session replay tools (like LogRocket) complement this by recording user interactions leading up to an exception.
Pitfall Guide
1. Leaving debugger Statements in Production Bundles
Explanation: debugger is a valid JavaScript statement. If not stripped during the build process, it will pause execution in production browsers, freezing the UI for end users.
Fix: Configure your bundler (Webpack, Vite, Rollup) to remove debugger statements in production mode. Use terser or esbuild minification with drop_debugger: true.
2. Overusing console.log for Asynchronous State
Explanation: Logging inside async callbacks or promises often captures stale or mutated state due to JavaScript's event loop and closure behavior.
Fix: Use debugger or conditional breakpoints to pause exactly when the async operation resolves. Inspect the resolved value in the scope panel instead of relying on logged snapshots.
3. Ignoring Source Map Configuration in CI/CD
Explanation: Minified bundles without uploaded source maps produce unreadable stack traces in error tracking systems. Developers waste hours reverse-engineering line numbers.
Fix: Automate source map upload in your deployment pipeline. Use Sentry CLI, Vercel source map uploads, or custom scripts to push .map files to your error tracking provider immediately after build.
4. Misinterpreting Heap Snapshots Without Baselines
Explanation: Taking a single heap snapshot shows current memory usage but doesn't reveal leaks. Developers often mistake normal garbage collection pauses for memory leaks.
Fix: Take three snapshots: baseline, after interaction, and after forced garbage collection. Compare the second and third snapshots to identify objects that persist despite GC. Focus on "Detached DOM trees" and "Closure" retainers.
5. Blocking Network Requests Without Dependency Awareness
Explanation: Using the Network tab to block third-party scripts or analytics can break application functionality if those requests are tied to critical initialization flows.
Fix: Use pattern-based blocking (*analytics* or *tracker*) rather than exact URLs. Verify application behavior after blocking, and use the "Preserve log" option to ensure blocked requests don't interfere with debugging sessions.
6. Using setInterval Without Lifecycle Cleanup
Explanation: Timers and event listeners attached in components or modules often persist after navigation or unmount, causing memory leaks and duplicate executions.
Fix: Always pair setInterval/addEventListener with cleanup logic. In React, use useEffect return functions. In vanilla JS, store references and call clearInterval/removeEventListener on teardown.
7. Relying on console.dir Without Depth Limits
Explanation: Logging deeply nested or circular objects with console.dir can cause performance degradation or console crashes due to infinite traversal.
Fix: Explicitly set depth limits: console.dir(obj, { depth: 2 }). For circular structures, use JSON.stringify(obj, null, 2) with a replacer function, or inspect via the Scope panel in DevTools.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Local development of complex state logic | debugger + Scope inspection | Provides deterministic pause, full variable access, step-through execution | Zero infrastructure cost |
| High-frequency event debugging (scroll, resize, animation) | Logpoints + Conditional breakpoints | Avoids UI freezing, filters irrelevant iterations, no source modification | Zero infrastructure cost |
| Production error reproduction | Source maps + Error tracking SDK | Maps minified stacks to original code, captures user context and session data | Low SaaS cost, high MTTR reduction |
| Memory leak investigation | Heap snapshot comparison + Allocation sampling | Identifies persistent objects, closure retainers, and detached DOM nodes | Zero infrastructure cost |
| Network-dependent feature testing | Network throttling + Request blocking | Simulates poor connectivity, isolates third-party dependencies | Zero infrastructure cost |
Configuration Template
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Debug Frontend",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"]
},
{
"type": "node",
"request": "launch",
"name": "Debug Node API",
"runtimeExecutable": "node",
"runtimeArgs": ["--inspect"],
"program": "${workspaceFolder}/src/server.ts",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
]
}
// vite.config.ts (Source Map & Production Optimization)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
sourcemap: 'hidden', // Generates .map files but hides URL from bundle
minify: 'esbuild',
rollupOptions: {
output: {
sourcemapIgnoreList: (relativeSourcePath) => relativeSourcePath.includes('node_modules'),
},
},
},
server: {
open: true,
hmr: true,
},
});
Quick Start Guide
- Initialize Inspector Configuration: Add
debugger statements at critical state transitions in your TypeScript modules. Open DevTools (F12) and verify the Sources panel loads your original files.
- Configure Conditional Breakpoints: Right-click line numbers in high-frequency loops or event handlers. Set conditions like
payload.type === 'UPDATE' or counter > 50 to pause only on relevant executions.
- Deploy Source Maps: Enable
sourcemap: 'hidden' in your build config. Add a post-build script to upload .map files to your error tracking provider. Verify stack traces in production match original line numbers.
- Instrument Error Tracking: Initialize your error tracking SDK with environment detection and custom context. Wrap risky operations in try/catch blocks and attach metadata tags for filtering.
- Validate Memory & Performance: Use the Memory tab to capture heap snapshots before and after user interactions. Switch to the Performance tab to record UI actions and identify long tasks exceeding 50ms. Apply throttling profiles to test degradation gracefully.