Browser DevTools are the workshop where you find out why a page is not doing what you think it should. The Console panel answers quick questions, the Sources panel catches the ones that only appear at runtime, and a handful of console commands make both dramatically faster. Here is a workflow you can use on every bug from your first week onward.
Which DevTools panel answers which question?
DevTools is a big UI, and beginners tend to bounce between panels without a plan. Each panel answers a different kind of question.
| Panel | The question it answers |
|---|---|
| Console | What does this code produce, and what error did it throw? |
| Sources | Which line is running, and what are my variables right now? |
| Elements | What does the DOM look like right now, and did my script change it? |
| Network | What requests did the page make, and what came back? |
| Application | What is stored in localStorage, cookies and the cache? |
| Performance | Where did the time go? |
Most of the time you need two of them at once: Console on the bottom, Sources in the main area, docked side by side. Press F12 on Windows and Linux, or Cmd + Option + I on macOS. If nothing happens, the browser probably opened it in a separate window; drag the panel back to the bottom, or use the three-dot menu in the top right of DevTools to choose Undock into separate window the other way round.
What can the console do besides print?
It is an evaluation environment. Anything you type is evaluated as JavaScript and its result is shown, which makes it the fastest way to poke at a live page.
// Grouped output keeps a noisy console readable.
console.group("Cart");
console.log("items:", 3);
console.log("total:", 249.5);
console.groupEnd();
// A running counter, handy inside a loop.
for (const step of ["add", "remove", "clear"]) {
console.count(step);
}
// Timing a block without leaving the page.
console.time("parse");
const payload = JSON.parse('{"items": 3}');
console.timeEnd("parse");
console.log(payload.items);
The console.count calls print the label followed by a running total, so you can see how many times each label fired. The console.time and console.timeEnd pair prints the elapsed milliseconds for that label when it ends. Neither exists in the browser console panel by default — they are Node.js additions — so the snippet above runs under node, not in the browser console.
Two more console-only helpers are worth stealing. copy(anything) puts the value on your clipboard, and table(rows) renders an array of records as an aligned table that is far easier to compare than a vertical list:
const rows = [
{ city: "Pune", rainfall: 82 },
{ city: "Delhi", rainfall: 24 },
{ city: "Kochi", rainfall: 118 },
];
console.table(rows);
Three cities, three rainfall figures, one aligned table. When you are comparing records this is the difference between scanning and reading.
How do breakpoints actually work?
A breakpoint is an instruction to the engine to pause before running a particular line. Open the Sources panel, find your file in the file tree, and click the line number to set one. When the code reaches it, execution halts and you can inspect the live state of every variable in scope.
Three controls matter while paused. Step over runs the current line and stops at the next one in the same function, ignoring function calls. Step into descends into a called function. Step out finishes the current function and returns to its caller. The Watch panel on the right lets you pin expressions so their values update on every step, which is how you watch a variable change without adding console noise.
You can also set a breakpoint that only fires under a condition. Right-click the line number and choose Add conditional breakpoint, then type a condition such as user.id === 42. The debugger now pauses only for that user, and you skip the breakpoint on every other run. For the same effect without stopping, use a logpoint: it prints a message to the console every time the line runs and never pauses, which is often better than a breakpoint because the program keeps its pace.
What is the debugger statement for?
There is a keyword you can drop into any line of code to make the DevTools open at that exact point. It is useful when you do not want to hunt for the line in a large file.
function calculateShipping(weightKg, zone) {
debugger;
const basePaise = 450;
const perKgPaise = zone === "remote" ? 225 : 110;
return basePaise + weightKg * perKgPaise;
}
console.log("standard:", calculateShipping(12, "city"));
console.log("remote:", calculateShipping(12, "remote"));
Run that with node and nothing appears to happen — the statement is inert unless a debugger is attached. Open the file in the browser Sources panel with DevTools open, and the browser pauses on that line with every variable in scope visible. The two lines after it then run one at a time as you step through.
Two cautions. The debugger statement ships to production if you forget it, and it will freeze the page for any user whose DevTools are open. Treat it like a console.log: remove it before you commit.
How do you inspect a live value precisely?
The browser console has a handful of variables that refer to what you currently have selected in the Elements panel. $0 is the selected element, $1 the previously selected one, and $_ is the last expression you evaluated. $$("selector") returns every element matching a CSS selector as an array, and copy() drops a value into your clipboard.
// Inspect the first matching element on the page.
const heading = document.querySelector("h1");
console.dir(heading);
console.log("text:", heading.textContent);
That snippet needs a page to run against, which is why it lives in the browser console rather than in a Node script. In the Elements panel, click an element, then type $0.className in the console and you get that element's classes without touching your source at all. To capture a value rather than read it, type copy($0.outerHTML) — copy() is a DevTools helper, not a language feature, so it only exists inside the console.
One subtlety worth knowing: when the browser console displays an object, it evaluates getters at the moment you expand it. If an object has a getter that throws, expanding it in the console can produce an error that never happens in normal use. console.dir on a plain data object has the same behaviour, which is another reason to prefer logging plain values during a hunt.
What workflow should you follow when something breaks?
Reproduction first, always. Open DevTools, reproduce the problem, and note the exact error message and the first line of the stack trace. That stack trace is the most informative thing on the screen and it is the part people skip.
Then narrow the scope. The error message tells you the type: a TypeError usually means a value was not what you assumed, a ReferenceError means a name does not exist in that scope, and a SyntaxError means the file did not even parse. That distinction tells you whether to set a breakpoint (runtime problems) or to reread the file (syntax problems).
Then inspect rather than guess. Set a breakpoint just before the wrong value is produced, open the Watch panel, and look at the data. Guessing at the cause produces code that logs more and fixes nothing.
Then fix and re-verify in the same session. A fix you have not re-run is a hypothesis.
The short version
- Console for questions, Sources for state, Elements for the DOM, Network for requests, Application for storage.
- The console evaluates expressions.
$0,$$andcopy()save you opening files. - Step over, step into and step out are the three controls you will use most while paused.
- Conditional breakpoints and logpoints beat unconditional ones: they narrow the noise instead of adding to it.
- Reproduce, read the stack trace, narrow the type of error, inspect the live values, fix, then re-run.
Try it yourself: take the counter page from the previous lesson and break it on purpose — remove the defer attribute so the script runs before the button exists. Open DevTools, reload, and read the error and the stack trace. Then set a conditional breakpoint on the click handler, click the button five times, and watch the counter variable update in the Watch panel on every step. Fix the ordering, reload, and confirm the error is gone. Reading the failure is faster than guessing at it.