Debugging in devtools properly
Module 1 said to take console.log out before shipping and promised a better
tool. Here it is. The difference is not convenience: a breakpoint lets you look
at everything at a moment in time, where a log shows you only what you thought
to ask for before you knew what was wrong.
Why logging runs out
function total(orders) {
console.log('orders', orders);
const paid = orders.filter((o) => o.paid);
console.log('paid', paid);
const sum = paid.reduce((t, o) => t + o.plates * 80, 0);
console.log('sum', sum);
return sum;
}
To answer one question you edited the function, reloaded, read the output, guessed again, and edited it again. Four cycles, and now you have to remove six lines.
A breakpoint answers all of those at once — plus the questions you had not thought of — without changing the file.
console.log is still fine for a quick check or a value you want on every
run. It stops being the right tool the moment you are exploring.
Setting a breakpoint
Open Sources in devtools, find your file, click a line number. Reload or trigger the code. Execution stops before that line runs, and the page freezes with everything still in place.
Now, without touching your code:
Scope shows every variable in reach — local, closure and global. This is module 3's scope chain, made visible, and it is the fastest way to understand a closure you are not sure about.
Call stack shows how you got here. Click any frame to inspect its variables — invaluable when a function is called from three places and misbehaves from one.
Console still works, and runs in the paused scope. Type a local variable name and see it. Try the expression you are unsure about. This is the single most useful thing in the panel.
Watch keeps an expression on screen, re-evaluated at every step.
Stepping
| Control | Does | Shortcut |
|---|---|---|
| Resume | Carry on until the next breakpoint | F8 |
| Step over | Run the next line, not into calls | F10 |
| Step into | Go into the function being called | F11 |
| Step out | Finish this function, return to the caller | Shift + F11 |
Step over is the one you want most: walk down a function watching values change. Step into when you reach the call you actually suspect.
The breakpoints worth knowing
A conditional breakpoint — right-click a line number, Add conditional breakpoint:
order.id === 47
Stops only when that is true. This is the one that changes how you debug. Three thousand rows and the bug is on one of them: the condition finds it, where stepping would take all afternoon.
A logpoint prints an expression without stopping and without editing the
file — right-click, Add logpoint. A console.log you cannot forget to remove.
debugger in your code stops there when devtools is open:
function total(orders) {
debugger;
return orders.reduce((t, o) => t + o.plates * 80, 0);
}
Useful when the code is hard to find in Sources — inside a template string, or generated. It is still a line in your file, so it must come out before you ship. It is the only one here with that problem.
Pause on exceptions — the pause-symbol button at the top of Sources. The debugger
stops at the moment an error is thrown, with the scope intact. Tick "Pause on
caught exceptions" too when an error is being swallowed by a try/catch you
did not write.
For an error you cannot reproduce on demand, this is usually the fastest route to it.
Event listener breakpoints — pause on any click, or any fetch. Good for
"something happens when I click and I cannot find the code".
Debugging async code
Module 8's code is where stepping feels strange: step over an await and you
land somewhere unrelated, because the thread went off to do other work.
Two things help.
The call stack shows async frames. Devtools stitches the chain together, so
you can see which await you came from — not just the microtask that resumed
you.
Break on the await, not inside the promise. Put the breakpoint on the line
after the await and let it run; you get the resolved value in scope with the
stack intact.
For a failing request, the Network panel is faster than any breakpoint. It tells you the status, the headers, the response body and the timing. Module 8's whole "three outcomes" distinction — worked, server said no, could not ask — is visible there in one glance, and it is where you confirm whether the problem is yours or the server's.
Reading an error properly
Before any of this, read the error. Module 1's three parts, now with the stack:
Uncaught TypeError: Cannot read properties of undefined (reading 'plates')
at total (orders.js:12:34)
at render (app.js:45:18)
at HTMLButtonElement.<anonymous> (app.js:60:5)
Read the stack bottom-up to see how you got here, top-down to find where it
broke. The top frame is where it threw — orders.js line 12. The frames below
are who called it.
orders.js:12:34 is a link. Click it and Sources opens at that character.
The stack often points at a library, not your code. Scroll down to the first frame that is yours — that is usually where the wrong value came from. A library throwing means you handed it something it did not expect.
Source maps
Built or minified code is unreadable:
function t(e){return e.reduce((t,n)=>t+n.p*80,0)}
A source map lets devtools show your original file while running the built one. Bundlers generate them, and devtools uses them automatically.
If you are looking at minified code in Sources, source maps are missing or off — check the bundler's config rather than trying to read it. Debugging minified code by hand is a waste of an afternoon.
Other panels worth a visit
| Panel | For |
|---|---|
| Network | Requests, status, payloads, timing. Throttle to Slow 3G |
| Application | localStorage, cookies — module 7 |
| Elements | The live DOM, and what your code actually produced |
| Performance | Why something is slow. Record, then look for long tasks |
| Lighthouse | An audit of performance and accessibility |
Throttling is the one to make a habit. Set Slow 3G and use your own page. Everything about your loading states becomes obvious, and your users in India on mobile data are the reason.
A method
Tools do not debug; you do. The habit that works:
Reproduce it reliably first. A bug you cannot trigger on demand cannot be confirmed fixed. This step is skipped constantly and costs the most time.
Read the error and the stack. Often enough on its own.
Form one specific guess. Not "something is wrong with the total" but "I think
orders is empty by the time total runs".
Check that one guess — breakpoint, watch, or the Network panel.
Change one thing. Changing three and finding it works leaves you not knowing why, which means it will come back.
Then ask why it happened. A missing await is a fix; noticing that three
other functions have the same shape is the thing that pays.
Check your work
A breakpoint shows every variable at a moment; a log shows one value you thought to ask for in advance.
The Console runs in the paused scope, so you can evaluate local variables and test expressions there.
Step over runs the next line without entering calls; step into enters the call; step out finishes the current function.
A conditional breakpoint stops only when an expression is true, which is how you find one bad row in three thousand.
A logpoint prints without stopping and without editing the file.
debugger is a real line that must be removed before shipping.
Pause on exceptions stops where an error is thrown, with scope intact — including caught ones, if you tick that box.
For a failed request the Network panel beats a breakpoint, because it shows status, body and timing together.
Read a stack trace top-down for where it broke, bottom-up for how you got there, and find the first frame that is your own code.
Minified code in Sources means source maps are missing.
Practice
- Take a function with a bug and debug it twice — once with
console.log, once with a breakpoint. Time both. - Pause inside a function and read the Scope panel. Find a local, a closure variable and a global.
- Use the console while paused. Evaluate a local variable and then an expression you are unsure about.
- Step over a whole function, then run it again stepping into the first call.
- Click through the call stack and confirm the variables change with the frame.
- Loop over fifty items and set a conditional breakpoint that stops only on number 37.
- Replace a
console.logwith a logpoint and confirm you get the output with no code change. - Add a
debuggerstatement, use it, then remove it. Note that steps 6 and 7 did not need removing. - Turn on Pause on exceptions and trigger an error. Then wrap that code in
try/catchand confirm you need "pause on caught exceptions" to stop there. - Break on the line after an
awaitin your module 8 page and inspect the resolved value. - Open Network, throttle to Slow 3G, and reload your orders page. Watch the loading state you wrote in module 8 actually do its job.
- Harder. Take the orders page, introduce three bugs without looking — a
missing
await, a wrong property name, and an off-by-one — then find each using a different tool: the stack trace, a conditional breakpoint, and the Network panel. Write one sentence per bug about which tool would have been slowest.
That is module nine. Your code can be split across files, use packages other people wrote, and be debugged with something better than guessing. That is the toolchain, and there is not much more to it than this at the level you need now.
Next: the capstone. One project, built from nothing to deployed, using every module in this course.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship