Debugging is not about luck — it's a skill. This guide gives you a systematic toolkit: from console.log to Chrome DevTools breakpoints to async error tracing in Node.js. Work through these techniques once and you'll squash bugs in minutes instead of hours.
Quick-reference table
| Technique | When to use |
|---|---|
console.log / console.table |
Fast sanity checks on values |
debugger statement |
Pause execution at an exact line |
| DevTools Breakpoints | Step through code without touching source |
| DevTools Network tab | Debug fetch/XHR failures |
| DevTools Console | Run expressions in the current scope |
try/catch + console.error |
Catch and surface runtime errors |
| Source maps | Debug minified/transpiled code in production |
node --inspect |
Debug Node.js in Chrome DevTools |
| VS Code debugger | Full IDE debugging with watch variables |
performance.now() |
Pinpoint slow code sections |
1. Console methods beyond console.log
Most developers use only console.log. The full API is much richer.
// Basic output levels
console.log('Info:', value);
console.warn('Warning:', value); // yellow in DevTools
console.error('Error:', value); // red + stack trace
// Tabular output — perfect for arrays of objects
const users = [{ id: 1, name: 'Ana' }, { id: 2, name: 'Ivo' }];
console.table(users);
// Grouping related output
console.group('User validation');
console.log('name:', name);
console.log('email:', email);
console.groupEnd();
// Timing a block of code
console.time('fetchUsers');
await fetchUsers();
console.timeEnd('fetchUsers'); // → fetchUsers: 143.2ms
// Assert — only logs if condition is false
console.assert(arr.length > 0, 'Array must not be empty', arr);
// Stack trace at any point
console.trace('Called from here');
// Count how many times a point is hit
function render() {
console.count('render'); // → render: 1, render: 2 …
}
Tip: console.log({ value }) prints { value: 42 } — the variable name is shown automatically because you're using shorthand object notation.
2. The debugger statement
Insert debugger anywhere in your code. When DevTools is open, execution pauses at that line — just like a breakpoint, but in source.
function calculateTotal(items) {
debugger; // DevTools pauses here
return items.reduce((sum, item) => sum + item.price, 0);
}
Remove it before committing — linters (ESLint no-debugger rule) catch stray debugger statements in CI.
3. Chrome DevTools: breakpoints and step execution
Open DevTools with F12 or Ctrl+Shift+I (Windows/Linux) / Cmd+Option+I (macOS).
Setting a breakpoint
- Go to the Sources tab.
- Open your file in the file tree.
- Click a line number — a blue marker appears.
- Reload or trigger the code path.
Stepping controls
| Button | Shortcut | Action |
|---|---|---|
| ▶ Resume | F8 | Continue to next breakpoint |
| ↷ Step over | F10 | Run the current line, stay in this function |
| ↓ Step into | F11 | Enter the function called on this line |
| ↑ Step out | Shift+F11 | Finish current function, return to caller |
The Scope panel
While paused, the Scope panel on the right shows all variables in the current closure — local, closure, and global scopes. Hover over a variable in the source to see its live value.
Conditional breakpoints
Right-click a line number → Add conditional breakpoint. Enter an expression like user.id === 42. The breakpoint only fires when the condition is true — ideal for loops with thousands of iterations.
Logpoints (log without pausing)
Right-click a line number → Add logpoint. Enter a message like "price: " + item.price. The expression is logged to the Console every time execution hits that line — no console.log clutter in source.
4. Debugging network requests
In the Network tab:
- Filter by Fetch/XHR to isolate API calls.
- Click a request to see Headers, Payload (request body), Preview and Response.
- Check the Timing subtab for slow requests (TTFB, download time).
- Throttle the connection with the dropdown (Fast 3G, Slow 3G) to test offline / slow-network behaviour.
5. JavaScript error types
Understanding the error type tells you where to look.
| Error | Cause | Example |
|---|---|---|
SyntaxError |
Invalid JS syntax (parse time) | if (x { |
ReferenceError |
Variable not declared | console.log(foo) before let foo |
TypeError |
Wrong type or undefined method |
null.toString() |
RangeError |
Value out of allowed range | new Array(-1) |
URIError |
Malformed URI in decodeURIComponent |
decodeURIComponent('%') |
EvalError |
eval() misuse |
(rare in modern code) |
Read the stack trace bottom-up — the bottom frame is where the error originated; the top is where it surfaced.
6. Debugging async code
Async bugs are the trickiest because errors surface far from their cause.
Unhandled promise rejection
// WRONG — swallows the rejection
fetchData().then(process);
// CORRECT — always chain .catch()
fetchData()
.then(process)
.catch(err => console.error('fetchData failed:', err));
// With async/await
async function load() {
try {
const data = await fetchData();
process(data);
} catch (err) {
console.error('load failed:', err, err.stack);
}
}
Enable "Pause on caught exceptions" and "Pause on uncaught exceptions" in DevTools Sources → the ⏸ icon. This pauses execution at the exact throw site.
Async stack traces
Modern Chrome DevTools (v73+) captures async call stacks automatically. You'll see async frames in the call stack panel labelled "Async call from …".
For Node.js, set Error.stackTraceLimit = 50 at startup to capture deeper stacks.
7. Source maps
If you deploy minified/transpiled code (TypeScript, bundled React, etc.) you'll see unhelpful stack traces like bundle.js:1:4523. Source maps fix this.
In Vite / webpack, source maps are enabled automatically in development mode. For production debugging, generate a separate .map file and configure your error tracker (Sentry, Datadog) to upload it privately — never serve the .map file publicly.
// vite.config.ts — enable source maps in build
export default {
build: {
sourcemap: true, // or 'hidden' to not serve publicly
}
}
In Chrome DevTools, go to Settings → Preferences → Sources and enable "Enable JavaScript source maps" (default: on).
8. Node.js debugging
Option A: node --inspect
node --inspect server.js
# Node listens on ws://127.0.0.1:9229
# Or break at start
node --inspect-brk server.js
Open chrome://inspect in Chrome → click inspect under "Remote Target". You get the full DevTools UI connected to your Node process.
Option B: VS Code debugger
Create .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug server",
"program": "${workspaceFolder}/server.js",
"skipFiles": ["<node_internals>/**"]
}
]
}
Press F5 to start. Set breakpoints by clicking the gutter. The Watch panel lets you evaluate expressions live. The Debug Console at the bottom accepts any JS expression in the current scope.
9. Performance debugging
When your app is slow:
// Quick timing
const start = performance.now();
heavyCalculation();
console.log(`Took ${(performance.now() - start).toFixed(2)}ms`);
For deeper analysis: DevTools Performance tab → Record → interact with the page → Stop. Look for long tasks (red bars), layout thrashing (interleaved reads/writes), and excessive re-renders.
The Memory tab's Heap Snapshot catches memory leaks: take a snapshot, do an action, take another — compare "Alloc. between snapshots" to see what's accumulating.
6 common debugging mistakes
| Mistake | Fix |
|---|---|
Using == instead of === for comparisons |
Always use ===; type coercion hides bugs |
| Mutating state directly in React | Use spread/structuredClone; state should be immutable |
console.log objects by reference |
Use console.log(JSON.parse(JSON.stringify(obj))) to snapshot the value at that moment |
| Ignoring the call stack | Read the full stack trace, not just the top line |
| Commenting out code to isolate bugs | Use binary search: disable half the code at a time |
Leaving debugger in production |
Add ESLint no-debugger rule to prevent it |
FAQ
Q: Why does console.log show an object but its properties are empty?
The DevTools console evaluates objects lazily. By the time you expand the object, its properties may have changed. Use console.log(JSON.stringify(obj)) or structuredClone(obj) to freeze the snapshot.
Q: Breakpoints disappear after a page reload. Why?
If DevTools opens after the breakpoint file loads, the source map may not match. Try: check Settings → Blackboxing hasn't excluded your file, and ensure the file hasn't been chunked differently by your bundler.
Q: How do I debug a TypeError: Cannot read properties of undefined?
Walk up the call stack. The object that's undefined was likely not returned or was overwritten earlier. Add a breakpoint before the line and inspect the variable in the Scope panel.
Q: How do I debug a production error with no source maps?
Use an error tracker (Sentry, Datadog, Bugsnag). Configure it to upload source maps at deploy time so traces are de-minified in the dashboard — without exposing .map files publicly.
Q: What's the difference between Step Over and Step Into?
Step Over (F10) runs the current line and stops at the next line in the same function — it skips into any function calls on that line. Step Into (F11) enters the first function called on the current line.
Q: How do I find which code is causing a UI freeze?
Open the DevTools Performance tab, click Record, reproduce the freeze, stop recording. Look for long Task blocks (>50ms is "long task"). The flame chart shows which function took the most CPU time.