What tools and techniques do you use for debugging JavaScript code?
TL;DR
Reproduce the failure reliably, reduce it to the smallest useful scenario, form a specific hypothesis, and inspect the program at the boundary where expected and actual behavior diverge. Use breakpoints and the call stack for control flow, the Network panel for request failures, source maps for transformed code, the Performance and Memory panels for measured performance problems, and framework-specific tools only when the failure is inside that framework's state or render model.
Prefer a debugger, conditional breakpoint, or logpoint over scattering permanent console.log() calls. Preserve the failing input and add a regression test after finding the cause.
A practical debugging workflow
Suppose clicking “Save” sometimes shows an old value:
- Reproduce it with exact steps and record the browser, input, account state, and timing.
- Inspect the boundary in the Network panel. Did the expected request run? Was its payload current? What status and response came back?
- Pause execution in the click handler or request wrapper. Inspect local variables, closures, and the call stack rather than logging only the final value.
- Use a conditional breakpoint such as
draft.id === failingIdwhen the code runs too often. - Follow asynchronous flow with async stack traces and Promise rejection breakpoints. Check whether an older response overwrote a newer one.
- Change one cause—for example, abort the superseded request—and repeat the exact scenario.
- Add a regression test that resolves requests out of order so the race cannot return unnoticed.
This separates a UI-state bug from a request, server, or race-condition bug before changing code.
Browser developer tools
Sources and debugger
- Line, conditional, DOM, event-listener, and exception breakpoints pause at useful moments.
- Step over, into, and out while inspecting scopes, watches, and the call stack.
- Logpoints print an expression without editing and redeploying source code.
- Source maps connect bundled or transpiled output to authored files. Verify that the deployed map matches the deployed bundle; a stale map sends debugging to the wrong line.
debugger;is useful when the pause condition is easiest to express in code, but remove it before shipping.
Console
Use console.error(), console.table(), console.trace(), and grouped or labeled logs to answer a question. Logging objects can be misleading because some consoles show their later state when expanded; log a snapshot when the value at that moment matters.
Never log access tokens, passwords, full payment data, or unnecessary personal information. Remove noisy diagnostic logging or place it behind an appropriate debug mechanism.
Network and Application panels
Inspect URL, method, headers, request payload, status, response, initiator, timing, redirects, cache behavior, and CORS messages. Use the Application panel for cookies, storage, service workers, and cache state. “Works after clearing site data” is evidence of a state or versioning problem, not a complete fix.
Performance and Memory panels
Use the Performance panel when a recorded interaction is slow or janky and the Memory panel when retained memory grows across repeated actions. Do not use timestamps around arbitrary code as proof that one implementation is globally faster; profile the user-visible scenario.
Runtime and framework tools
Node.js supports inspector-based breakpoints, CPU profiles, heap snapshots, diagnostic reports, and --trace-* options for specific runtime problems. React DevTools, Vue Devtools, and Redux DevTools can inspect component or store state, but they complement rather than replace browser and network debugging.
For production-only failures, use structured logs, error reporting, request or trace IDs, and version metadata to correlate the client symptom with server work. Treat source maps, traces, heap snapshots, and logs as potentially sensitive artifacts.
Common debugging mistakes
- Changing several things before reproducing the bug again.
- Catching and ignoring an error, which removes the stack and converts a clear failure into stale state.
- Debugging minified output without the matching source map.
- Assuming a request succeeded because
fetch()fulfilled; HTTP error responses still produce aResponse. - Adding a delay that hides a race instead of controlling ordering or cancellation.
- Fixing only the observed input without identifying the violated invariant.
Further reading
- Chrome DevTools: JavaScript debugging
- Chrome DevTools: Network panel
- Chrome DevTools: Performance panel
- Chrome DevTools: Memory panel
- Node.js: Debugging