How to Debug JavaScript in Chrome DevTools
Most developers spend hours sprinkling console.log() everywhere, then wonder why bugs still escape to production. Chrome DevTools gives you a systematic, surgical alternative — and this guide covers everything from basic breakpoints to async failures, memory leaks, and remote debugging on real devices.
Master Breakpoints and Step-Through Debugging

Setting and Managing Breakpoints
The wrong way: adding console.log(state) on 12 different lines and reloading the page six times. The right way: open Sources → your file, click the line number gutter, and let the debugger pause execution exactly where you need it.
Chrome supports three breakpoint types you should actually know:
- Line breakpoints — click any line number to pause there unconditionally
- Conditional breakpoints — right-click a line number → "Add conditional breakpoint" — only pauses when your expression is truthy
- DOM breakpoints — right-click a DOM node in Elements → "Break on" → subtree modifications, attribute changes, or node removal
// Conditional breakpoint expression — paste this in the breakpoint dialog
// Pauses only when cart total exceeds threshold
state.cartTotal > 100 && state.userId !== null
// You can also force a breakpoint from code itself (useful in complex builds)
function processOrder(state) {
debugger; // DevTools pauses here unconditionally when DevTools is open
return state.items.reduce((total, item) => total + item.price, 0);
}
Manage all breakpoints in the Breakpoints panel (right sidebar in Sources). You can disable without deleting — useful when you want to re-enable later without hunting through files.
Step Through Code Execution
Three controls developers constantly confuse:
- Step Over (F10) — executes the current line, stays in current function. Use this when you don't care what happens inside a called function.
- Step Into (F11) — follows the call inside a function. Use this when that function is where your bug lives.
- Step Out (Shift+F11) — runs the rest of the current function and pauses at the caller. Use this when you stepped in by mistake.
function formatUser(user) {
const name = sanitize(user.name); // Step Into here if sanitize() is broken
const email = validate(user.email); // Step Over if validate() is fine
return { name, email };
}
function sanitize(str) {
// DevTools pauses here when you Step Into from formatUser()
// Call stack panel shows: sanitize → formatUser → caller
return str.trim().toLowerCase();
}
The Call Stack panel shows the full execution chain. Click any frame to jump to that context and inspect variables at that level — this alone replaces a dozen console.log calls.
Inspecting Variables and Scope
When paused at a breakpoint, the Scope panel shows Local, Closure, and Global scopes. Hover over any variable inline in the Sources editor to see its current value.
For tracking mutations across multiple pause points, use Watch Expressions: click the + in the Watch panel and type any valid JS expression. It re-evaluates at every pause.
// Watch expressions to add in DevTools Watch panel:
// state.items.length
// state.items.filter(i => i.quantity === 0)
// JSON.stringify(state.currentUser)
// These update live as you step through this function:
function applyDiscount(state, code) {
const discount = lookupCode(code); // Watch: discount
state.items = state.items.map(item => ({
...item,
price: item.price * (1 - discount)
}));
return state; // Watch: state.items[0].price to confirm the mutation
}
Debug Async Code, Promises, and Modern JavaScript Patterns
Debugging Async/Await Functions
Async errors are sneaky because stack traces get truncated at await boundaries. DevTools 2026 maintains the async call stack — enable it via the gear icon in Sources → check "Async stack traces".
// WRONG: swallowing the rejection silently
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json(); // If this throws, you'll see nothing in console
}
// RIGHT: explicit error handling that DevTools can pause on
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: Failed to fetch user ${id}`);
}
return await res.json();
} catch (err) {
console.error('[fetchUser] Error:', err.message);
throw err; // Re-throw so callers know something went wrong
}
}
Set DevTools to Pause on Uncaught Exceptions (the pause icon in Sources toolbar). This catches rejections that slip past your try/catch.
Tracing Promise Chains and Race Conditions
Race conditions show up as intermittent failures. A common symptom: Cannot read properties of undefined (reading 'data') — only on slow connections.
// Debugging Promise.race() — add timestamps to identify which resolves first
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timed out after 5000ms')), 5000)
);
const dataFetch = fetch('/api/data').then(r => {
console.log('[dataFetch] resolved at:', performance.now());
return r.json();
});
// In DevTools, set a breakpoint inside each promise callback
// to observe which one wins under throttled network conditions
Promise.race([dataFetch, timeout])
.then(data => processData(data))
.catch(err => console.error('[race] Lost:', err.message));
Use the Network tab's throttle dropdown (set to "Slow 4G") while stepping through this to reliably reproduce timeout paths.
Error Stack Traces and Exception Handling
Minified production stack traces look like at t (main.a3f9.js:1:4521) — useless without source maps. Configure your bundler to generate .map files and point DevTools at them via Settings → Workspace or the gear icon in Sources.
See the official Chrome DevTools source maps documentation for bundler-specific setup with webpack, Vite, and esbuild.
Network and API Debugging for Real-World Failures

Inspect Network Requests in the Network Tab
Filter by Fetch/XHR to cut noise. Click any request to see Headers, Payload, Preview, and Response. The Timing waterfall shows exactly where time is spent: DNS lookup, TCP connection, TTFB, and content download.
// Quick console snippet to test an API endpoint directly
// Run this in the DevTools Console tab — no file editing needed
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer test-token' },
body: JSON.stringify({ userId: 42, items: [{ id: 1, qty: 2 }] })
});
console.log('Status:', res.status);
console.log('Headers:', Object.fromEntries(res.headers.entries()));
const body = await res.json();
console.log('Body:', body);
For 401 errors: check the Authorization header in the Request Headers panel. For 500s: check the Response tab — most APIs return an error message body that your code may be ignoring.
Mock Network Responses and Test Offline Scenarios
Chrome DevTools has built-in request overrides via Network → right-click a request → "Override response". This lets you test error handling without changing your backend.
// Test that your UI handles a 500 gracefully
// 1. Open Network tab, find your /api/products request
// 2. Right-click → Override response
// 3. Set status to 500, body to: {"error": "Internal Server Error"}
// 4. Reload — your error handling code should trigger
// Your code should handle this:
async function loadProducts() {
const res = await fetch('/api/products');
if (!res.ok) {
// This branch is what you're testing with the mock
showErrorBanner(`Failed to load products (${res.status})`);
return [];
}
return res.json();
}
Simulate offline mode with the throttle dropdown → "Offline". Service workers that cache incorrectly will expose themselves immediately.
Console Debugging for API Issues
The Console isn't useless — it's just usually misused. Run fetch calls directly, inspect live DOM state, and test utility functions without touching your source files. Also check the Stack Decoded error handling guide for structuring API error responses cleanly.
Performance Profiling, Memory Leaks, and Advanced Scenarios
Detect Memory Leaks with Heap Snapshots
The symptom: your SPA slows down after 20 minutes of use. The cause is almost always a leaked event listener or a closure holding a reference to a removed DOM node.
- Open Memory tab → take a Heap Snapshot (baseline)
- Perform the action you suspect leaks (open/close a modal 10 times)
- Take a second Heap Snapshot
- Switch view to "Comparison" — sort by "# Delta" descending
- Look for your component class or "Detached HTMLDivElement" entries
// WRONG: event listener added on mount, never removed
function initModal(element) {
document.addEventListener('keydown', handleEscape); // Leaks on every mount
}
// RIGHT: cleanup on unmount
function initModal(element) {
const handleEscape = (e) => { if (e.key === 'Escape') closeModal(); };
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape); // Cleanup
}
Performance Profiling and CPU Throttling
Open the Performance tab → set CPU throttle to 4x slowdown (simulates a mid-range Android device) → click Record → trigger your slow operation → stop recording. The flame chart shows every function call and its duration. Look for wide orange bars.
Check the Stack Decoded performance optimization guide for fixing the bottlenecks you find.
Debugging Service Workers, WebWorkers, and Remote Debugging
Service Workers live in Application → Service Workers. You can force-update, unregister, and set breakpoints in their code via Sources. For WebWorkers, they appear as separate thread contexts in the Sources panel — breakpoints work the same way.
Remote debugging Android: enable USB debugging on the device, plug in via USB, navigate to chrome://inspect on your desktop Chrome. Your phone's browser tabs appear — click "inspect" to open a full DevTools session connected to the mobile browser. Same workflow works for debugging WebViews in hybrid apps.
Comparison: Debugging Approaches

| Method | Best For | Visibility | Speed |
|---|---|---|---|
| console.log() | Quick value checks | Values only | Fast to add, slow to debug |
| Line Breakpoints | Synchronous logic bugs | Full scope + call stack | No code changes needed |
| Conditional Breakpoints | Loops, specific state bugs | Full scope on condition | Skips irrelevant iterations |
| Async Stack Traces | Promise/await failures | Full async chain | Requires DevTools setting enabled |
| Heap Snapshots | Memory leaks | Object retention graph | Slower, use targeted |
| Network Overrides | API error path testing | Request/response full detail | No backend changes needed |
Frequently Asked Questions
Q: What's the difference between "Step Into" and "Step Over"?
A: Step Over (F10) executes the entire function call on the current line and lands on the next line in your current scope — use it when the called function isn't where your bug is. Step Into (F11) follows execution inside the called function — use it when you need to inspect what happens inside. If you Step Into a function by mistake, Step Out (Shift+F11) runs it to completion and returns you to the caller.
Q: Why isn't my breakpoint being hit?
A: Three common causes: your bundler is tree-shaking or dead-code-eliminating that branch so it never runs, your source map is misconfigured so DevTools thinks a different line is line 45, or the code already executed before you set the breakpoint (common in module initialization). Use debugger; as a fallback — it's hardcoded and bundler-agnostic. Also check the breakpoint icon in the gutter: a hollow circle means the source map can't resolve it.
Q: How do I debug code that only fails in production?
A: First, ensure your production build generates source maps — Vite and webpack both support this with sourcemap: true in config. Then use chrome://inspect or attach DevTools to the deployed site directly. If source maps can't be exposed publicly, reproduce the failure in a staging environment with identical build settings and use Network overrides to simulate the exact response that triggers the bug.
Wrap-up
Breakpoints and step-through execution are where you start; async stack traces, Network overrides, and heap snapshots are where you stop losing hours to hard bugs. Every technique here replaces at least five console.log statements with one precise pause. DevTools in 2026 continues to improve async visualization and memory tooling — keep the async stack traces setting enabled by default and make heap snapshot comparisons part of your pre-release checklist. Start by replacing your next debugging session's first console.log with a conditional breakpoint instead.
References
- Debug JavaScript | Chrome DevTools | Chrome for Developers
- Debug JavaScript in Chrome with DevTools | Tutorials
- How to Use Developer Tools to Debug JavaScript in the Browser
- How To Debug JavaScript In Chrome DevTools | DebugBear
- Debugging JavaScript Applications with Chrome DevTools - Medium
- Debug JavaScript | Chrome DevTools
Comments
Post a Comment