Most beginners do not lose hours to hard bugs. They lose hours to bad debugging habits. The bug itself is often a typo, an undefined property or a promise that resolved one tick too late. What turns a two-minute fix into an afternoon is the workflow around it: scattering console.log everywhere, skimming the stack trace, refreshing fifty times and changing three things at once.
This guide breaks down the ten JavaScript debugging mistakes we see most often, and pairs each one with the correct Chrome or Edge DevTools workflow plus a short code example showing the faster fix. Everything here works in the current version of Chrome, Edge, Firefox and Safari DevTools, and most of it applies to VS Code debugging too.
Quick reference: the 10 mistakes and their fixes
| # | The mistake | The faster habit |
|---|---|---|
| 1 | Using console.log as the only tool |
Breakpoints, logpoints and richer console methods |
| 2 | Logging objects and trusting what you see | Log a snapshot, not a live reference |
| 3 | Misreading the stack trace | Read top line first, then the call chain |
| 4 | Never using conditional breakpoints | Break only on the iteration that matters |
| 5 | Confusing async timing | Await properly and use async stack traces |
| 6 | Swallowing errors in empty catch blocks | Pause on exceptions, log the real error |
| 7 | Debugging bundled code with no source maps | Enable source maps and ignore-list vendor code |
| 8 | Guessing scope and this |
Read the Scope pane while paused |
| 9 | Blaming JavaScript when it is the network | Check the Network tab before the code |
| 10 | Changing several things at once | Minimal reproduction, one variable at a time |

Mistake 1: Treating console.log as your entire debugger
Why it wastes time: every log means editing a file, saving, reloading, clicking through the UI again, then repeating because you logged the wrong variable. Ten logs equal ten reload cycles. A breakpoint gives you every variable in scope in a single pause.
The faster DevTools workflow
- Open the Sources panel, press
Ctrl + P(Cmd + Pon Mac) and type the file name. - Click the line number to set a breakpoint, or right-click it and choose Add logpoint to print values without editing your source file.
- While paused, hover any variable to see its value, or type expressions directly in the Console.
// Slow: edit, save, reload, repeat
function getTotal(cart) {
console.log(cart);
let total = 0;
for (const item of cart) {
console.log(item.price, item.qty);
total += item.price * item.qty;
}
console.log(total);
return total;
}
// Faster: no code change at all
// Sources > right-click line number > Add logpoint:
// 'item', item, 'running total', total
When you do use the console, use the right method:
| Method | Use it for |
|---|---|
console.table(data) |
Arrays of objects, API responses, rows |
console.dir(node) |
Inspecting a DOM node as an object |
console.trace() |
Finding out who called this function |
console.count(label) |
Detecting re-renders and duplicate listeners |
console.time / timeEnd |
Measuring a slow block quickly |
console.assert(cond, msg) |
Logging only when an assumption breaks |
Mistake 2: Logging an object and trusting what the console shows
Why it wastes time: the console stores a reference to objects and arrays. When you expand the entry later, you see the object’s current state, not the state it had when you logged it. Beginners chase ghost bugs for hours because the log “proved” a property was already set.
The faster fix
const state = { ready: false };
console.log(state); // may expand later as { ready: true }
console.log({ ...state }); // snapshot of the value right now
console.log(structuredClone(state)); // deep snapshot
console.log(JSON.stringify(state)); // safest for plain data
Two more console settings beginners forget:
- Preserve log: keeps messages after a page reload or redirect, essential for form submissions.
- Log levels filter: if the console looks empty, check that Errors, Warnings, Info and Verbose are not filtered out.

Mistake 3: Misreading the stack trace
Why it wastes time: beginners read the message, panic, and start editing the file named at the bottom of the trace. A stack trace is read top to bottom: the first line is where it broke, the lines below show how you got there.
TypeError: Cannot read properties of undefined (reading 'name')
at renderUser (profile.js:42:21) // where it actually broke
at updateView (app.js:118:5) // who called it
at handleClick (app.js:90:3) // the original trigger
How to read it in 20 seconds
- Decode the message. “Cannot read properties of undefined (reading ‘name’)” means the object before
.nameis undefined, not thatnameis the problem. - Click the top frame link. DevTools jumps to the exact line and column.
- Walk down the frames in the Call Stack pane to see which arguments were passed in.
- Ignore-list your framework and node_modules so your own files stay visible in the stack.
Common error messages and what they really mean:
| Message | Real cause |
|---|---|
| Cannot read properties of undefined | The parent object does not exist yet or the key is misspelled |
| x is not a function | Wrong import, typo, or a value that is a string or undefined |
| Unexpected token < | You parsed an HTML error page as JSON |
| Cannot access ‘x’ before initialization | Temporal dead zone with let or const |
| Maximum call stack size exceeded | Infinite recursion, often a getter calling itself |
Mistake 4: Ignoring breakpoints, especially conditional ones
Why it wastes time: pressing Resume 400 times to reach the one broken record in a loop is not debugging, it is data entry. DevTools can pause only when a condition is true.
The faster DevTools workflow
- Conditional breakpoint: right-click a line number, choose Add conditional breakpoint, enter an expression such as
item.qty === undefined. - Event listener breakpoints: pause on any click, submit or keydown when you do not know which handler runs.
- DOM breakpoints: right-click an element in the Elements panel and break on attribute or subtree changes to catch the script that mutates it.
- XHR/fetch breakpoints: pause when a URL containing a given string is requested.
debugger;works too, but remove it before committing.
// Instead of logging 400 rows to find one bad record:
for (const item of cart) {
total += item.price * item.qty;
}
// Conditional breakpoint on the line above:
// typeof item.price !== 'number'
// You pause exactly on the offending item, with full scope available.
Useful keyboard shortcuts while paused:
| Action | Windows / Linux | Mac |
|---|---|---|
| Resume script | F8 | F8 |
| Step over | F10 | F10 |
| Step into | F11 | F11 |
| Open any file | Ctrl + P | Cmd + P |
| Search all files | Ctrl + Shift + F | Cmd + Option + F |
| Command menu | Ctrl + Shift + P | Cmd + Shift + P |
Mistake 5: Confusing async timing (the undefined that is not really undefined)
Why it wastes time: this is the single most common source of “my data is undefined but I can see it in the console”. The value is not missing, you are simply reading it before the promise settles.
// Mistake: the log runs before the response arrives
let user;
fetch('/api/user')
.then(r => r.json())
.then(data => { user = data; });
console.log(user); // undefined, every time
// Fix: await the value where you need it
async function loadUser() {
const res = await fetch('/api/user');
if (!res.ok) throw new Error('HTTP ' + res.status);
const user = await res.json();
console.log(user); // the real data
return user;
}
DevTools habits that make async bugs obvious
- Keep async stack traces enabled so the Call Stack shows the code that scheduled the callback, not just the microtask.
- Set a breakpoint inside the
.then()or after theawait, then check the Call Stack pane for the full async chain. - Use the Network panel timing bar to confirm whether the request even finished before your render ran.
- Remember the order: synchronous code, then microtasks (promises), then macrotasks (
setTimeout). AsetTimeout(fn, 0)still runs after every pending promise callback.
Another classic: forgetting that forEach does not await.
// Does not wait for anything
items.forEach(async (item) => { await save(item); });
// Waits properly
for (const item of items) { await save(item); }
// or, in parallel:
await Promise.all(items.map(save));

Mistake 6: Swallowing errors in empty catch blocks
Why it wastes time: an empty catch {} deletes the evidence. The app fails silently and you debug the symptom instead of the cause. Unhandled promise rejections are the same problem with extra steps.
// Mistake
try {
await saveOrder(order);
} catch (e) {}
// Fix: log the real error, keep the stack, rethrow if you cannot recover
try {
await saveOrder(order);
} catch (err) {
console.error('saveOrder failed', { orderId: order.id, err });
throw err;
}
// Safety net during development
window.addEventListener('unhandledrejection', (e) => {
console.error('Unhandled rejection:', e.reason);
});
In the Sources panel, turn on Pause on uncaught exceptions, and while hunting a stubborn bug also enable Pause on caught exceptions. DevTools then freezes the app at the exact moment the error is thrown, with every variable still alive.
Mistake 7: Debugging bundled code without source maps
Why it wastes time: you open the Sources panel, see t.default.a is not a function inside a minified bundle, and give up. With source maps you debug your original files, with your own variable names.
The checklist
- Enable source maps in your build (
sourcemap: truein Vite or Rollup,devtool: 'source-map'in Webpack). Dev servers usually enable them by default. - In DevTools Settings > Preferences, confirm Enable JavaScript source maps is checked.
- Add
node_modulesand framework internals to the Ignore list so stepping stays inside your code. - If a production bug is unreadable, reproduce it against a local build with source maps enabled rather than guessing from minified names.
// vite.config.js
export default {
build: { sourcemap: true }
};

Mistake 8: Guessing what is in scope instead of reading it
Why it wastes time: questions like “what is this here?” or “why is my closure holding the old value?” are answered instantly by the Scope pane. Guessing leads to random refactors.
const timer = {
label: 'checkout',
start() {
setTimeout(function () {
console.log(this.label); // undefined: `this` is not the object
}, 100);
}
};
// Fix: an arrow function keeps the surrounding `this`
start() {
setTimeout(() => console.log(this.label), 100);
}
While paused, the Scope pane lists Local, Closure, Block and Global scopes plus the current value of this. Add tricky expressions to the Watch pane and they update at every step. That removes the guesswork entirely.
Mistake 9: Blaming JavaScript when the problem is the network
Why it wastes time: a surprising share of “JavaScript bugs” are a 401, a CORS block, a 304 cached response or an endpoint returning an HTML error page. No amount of code reading will reveal that.
Check the Network panel first
- Filter by Fetch/XHR and look at the status column before touching the code.
- Open the Response tab: if it starts with
<!DOCTYPE html>, yourres.json()will throw “Unexpected token”. - Check Headers for CORS, auth tokens and content type.
- Tick Disable cache while DevTools is open to rule out stale bundles, a huge time sink when your changes seem to do nothing.
// fetch does not reject on 404 or 500, so check it yourself
const res = await fetch('/api/items');
if (!res.ok) {
throw new Error('HTTP ' + res.status + ' on ' + res.url);
}
const data = await res.json();
Mistake 10: Changing five things at once with no reproducible case
Why it wastes time: if you edit three files and the bug disappears, you do not know what fixed it, and it will come back. Debugging is an experiment: one hypothesis, one change, one observation.
The method that actually scales
- Reproduce reliably. Write the exact steps. If you cannot reproduce it, you cannot verify a fix.
- Shrink the case. Strip the page down to the smallest snippet that still fails.
- Form one hypothesis and state it out loud: “qty arrives as a string”.
- Test it with one change. Keep or revert, then repeat.
- Use version control.
git stashandgit bisectbeat memory every time.
// A 5-line reproduction beats staring at a 2000-line component
const cart = [{ price: '10', qty: 2 }];
console.log(getTotal(cart)); // '0102' -> string concatenation, found in seconds
// The real fix
const total = cart.reduce((sum, i) => sum + Number(i.price) * Number(i.qty), 0);

Bonus: three habits that prevent bugs before you debug them
- Enable strict mode and a linter. ESLint catches undeclared variables, unused values and forgotten
awaitcalls before the browser does. - Add type checking. TypeScript or JSDoc types eliminate most “undefined is not a function” errors at write time.
- Write the failing test first. A test that reproduces the bug is also your regression guard.
A 60-second debugging checklist
- Read the full error message and the top stack frame.
- Open DevTools and check the Console filters and Preserve log.
- Check the Network tab for failed or cached requests.
- Set a breakpoint at the top frame, not a console.log.
- Inspect Scope and Call Stack while paused.
- Form one hypothesis, make one change, retest.
- Remove stray logs and
debugger;statements before you commit.
FAQ
What is the most common JavaScript debugging mistake?
Using console.log as the only debugging tool. It forces a slow edit-save-reload loop and often shows stale or live-reference data. Breakpoints and logpoints give you the entire scope at the moment the code runs, with no file changes.
Is console.log bad practice?
No. It is fast and perfectly valid for a quick check. The mistake is relying on it exclusively, leaving logs in committed code, and logging mutable objects instead of snapshots. Pair it with breakpoints and console.table for structured data.
How do I read a JavaScript stack trace correctly?
Read from the top down. The first line is where the error was thrown, each line below is the function that called it, and the bottom is usually the original event or entry point. Click the top frame link to jump straight to that line in Sources.
Why does my variable show undefined even though the data exists?
Almost always an async timing issue: you read the variable before the promise or callback assigned it. Move the code that uses the value inside the await or .then() chain, and return the value instead of assigning it to an outer variable. It is argued more carefully on stackify.com.
What is the difference between a breakpoint and a logpoint?
A breakpoint pauses execution so you can inspect everything in scope and step through the code. A logpoint prints an expression to the console and keeps running, giving you the convenience of console.log without editing your source files.
How do I debug minified JavaScript in production?
Generate source maps during the build and make sure DevTools has source map support enabled. If maps are not available in production, reproduce the issue against a local or staging build that includes them, and ignore-list third-party bundles so your own frames stay readable.
Which DevTools panel should I open first?
Console for the error message, then Network to rule out failed or cached requests, then Sources for breakpoints. Opening Sources first and stepping blindly through code is usually the slowest path. JavaScript Debugging: the Complete Guide is a useful companion to this.
Final thoughts
None of these JavaScript debugging mistakes are about intelligence or experience with syntax. They are about workflow. Swap scattered logs for breakpoints, read stack traces top down, respect async timing and change one thing at a time, and most bugs that used to eat an afternoon will be closed before your coffee goes cold.

