← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 15 — Developer Tools and Debugging

Section 1 — What developer tools are

Every modern browser comes with a built-in set of developer tools, usually shortened to DevTools. They are not a separate program you have to download, and they are not a feature reserved for experts. They are part of the browser itself, sitting quietly behind the page until you open them, and they exist for one reason: to let you look inside a web page while it is running and understand what it is actually doing, rather than guessing from what it looks like on the surface.

That distinction matters more than it first sounds. When you write HTML, CSS and JavaScript, you are writing instructions, but the page your visitor sees is the browser's interpretation of those instructions. Sometimes the interpretation differs from your intention. A style does not apply, a script does nothing, an image never appears, or a layout collapses on a small screen. Without tools, you would be left staring at the finished page and wondering which of your hundreds of lines is responsible. Developer tools close that gap. They show you the page the way the browser sees it: the live document, the styles that actually won, the files that were requested, the errors that were thrown, and the code that is running right now.

Think of DevTools as a workshop fitted around the page. The Elements panel is the workbench where you examine structure and appearance. The Console is where the page talks to you, printing messages and errors, and where you can talk back by running code. The Sources panel is where you read the page's code files and pause them mid-run to inspect what is happening. The Network panel is a logbook of every file the page asked for and what came back. Other panels cover storage, performance, accessibility and more, and you will meet the important ones in this chapter. You do not need all of them at once. Most working developers spend the vast majority of their debugging time in just four panels: Elements, Console, Sources and Network. Learning those four well will carry you further than skimming all of them shallowly.

There is also a reassuring fact about DevTools that beginners often miss: everything you do inside them is local and temporary. Changing a colour, editing a heading or running a line of code in the Console affects only your own copy of the page in your own browser. It does not change the website on its server, and it does not affect other visitors. Refreshing the page throws your experiments away and restores the original. That makes DevTools a safe place to experiment. You can try a bold change, watch it fail, and lose nothing. The only way a DevTools experiment becomes permanent is if you deliberately copy the result into your own source files and save them, which is exactly the workflow this chapter teaches: experiment in the browser, then commit the good version to your code.

Check: What are browser developer tools for?

Section 2 — Opening developer tools

Opening developer tools is deliberately easy, because browser makers want developers to use them constantly. On most computers, pressing F12 opens them. On a Mac, the usual shortcut is Option plus Command plus I. If keyboard shortcuts are not your habit yet, right-click anywhere on a page and choose Inspect from the menu, and the tools open with the element you right-clicked already selected. Most browsers also list developer tools in their main menu, under a heading like More Tools, so there is always a route even if you forget the shortcut. Pick whichever method sticks in your memory and use it until it becomes reflex, because a tool you cannot open quickly is a tool you will not reach for when you are stuck.

When DevTools opens, it appears as a panel, and you can choose where that panel sits. Docked to the right of the page is a common choice, because it leaves the page readable while giving the tools room to breathe. Docked to the bottom works well on wide screens. You can also undock DevTools into its own separate window, which is handy if you have two monitors: page on one screen, tools on the other. The docking control is a small icon in the top-right corner of the DevTools panel, and changing it is harmless, so try each arrangement and keep the one that suits your screen. On a laptop, docking to the right with the panel at a comfortable width is usually the best compromise.

One thing that surprises beginners is that the page keeps working while DevTools is open. Nothing freezes or breaks just because the tools appeared. You can click buttons, fill in forms and scroll, and DevTools watches it all happen. This matters because many bugs only show up when you interact with a page: a menu that fails to open, a form that submits wrongly, a button that does nothing. With DevTools open beside the page, you can perform the action and watch the Console, Network and Elements panels respond at the same moment, which often points straight at the cause.

DevTools also stays open across page reloads within the same tab, which is useful: you can reload to reproduce a bug and watch the panels refill. And if you ever close DevTools by accident, reopening it is one keystroke away; nothing you did inside it is saved, but nothing is lost either, because your real files were never touched. A related habit worth building early is opening DevTools before you start debugging, not after. If a bug involves a request or a message that happens during loading, opening the tools after the page has loaded means you have already missed it. Open first, reload, watch. That ordering appears several times in this chapter, and it will save you from the confusing experience of an empty panel that should have shown something.

Check: Name two ways to open developer tools.

Section 3 — The Elements panel

The Elements panel is usually the first panel you see when DevTools opens, and it is the one you will visit most often. It shows the page's live DOM as a tree of elements, indented to show nesting: the html element at the top, head and body inside it, and every heading, paragraph, image, div and button nested beneath them in the order they appear. If you have ever drawn a family tree, the shape will feel familiar. Each line in the tree is an element, and you can expand or collapse it with the small arrow beside it to reveal or hide its children. Collapsing a large section you are not interested in makes a long page much easier to navigate.

It is important to understand what this tree is and is not. It is not a text copy of your HTML file. It is the DOM, the browser's live, structured model of the page, built by parsing your HTML and then updated by anything JavaScript does afterwards. If a script adds an element after loading, that element appears in this tree even though it is nowhere in your source file. If a script changes a piece of text, the tree shows the new text. That is why the Elements panel is the honest view of a page: it shows what the browser is actually working with right now, not what you wrote hours ago. Chapter 1 drew this distinction briefly; here it becomes a daily working tool.

You navigate the tree by clicking. Clicking an element selects it, highlights it on the page with a coloured overlay, and shows its styles in the pane beside the tree. You can also use the arrow keys to move up and down the tree, and the left and right arrows to collapse and expand. For a large page, the search box in the panel (opened with Ctrl or Command plus F while DevTools is focused) lets you find an element by tag name, class or id without scrolling through hundreds of lines. Learning to move through the tree quickly is a skill worth practising, because most debugging sessions start with the same question: which element is this, and where does it live?

The Elements panel also reveals things your source file hides. The browser sometimes fixes your mistakes silently: a missing closing tag may be closed for you, a misplaced element may be moved. The tree shows the corrected version, which can explain why a page looks different from what you expected. Similarly, elements created by browser extensions or by third-party scripts appear here, which is useful context when a page contains something you did not put there. None of this is a reason to be careless with your source; it is a reason to check the live tree when reality and expectation disagree.

Used well, the Elements panel answers the first question of almost every visual bug: what is this thing on the page, really? Once you know that, the Styles pane beside it answers the second question: why does it look this way? The two panels work as a pair, and the next sections take them one at a time.

Check: What does the Elements panel show you?

Section 4 — Inspecting one element

Knowing that the Elements panel holds a tree of the whole page is useful, but scrolling through that tree to find one button is slow. The inspect tool exists to skip the search. It is the small icon in the top-left corner of DevTools that looks like an arrow hovering over a box, and its keyboard shortcut is usually Ctrl or Command plus Shift plus C. Click it once to activate it, then move your mouse over the page. As you hover, each element under your cursor is highlighted with an overlay, and a small label shows its tag name, class and size. Click, and DevTools jumps straight to that element in the tree and selects it.

This is the fastest route from something you can see to the code behind it, and it changes how you approach visual problems. Instead of reading through your HTML file wondering which of twenty divs is the misaligned one, you click the misaligned thing and you are looking at it. The overlay itself carries information worth reading: it shows the element's dimensions and, in different colours, its margin and padding. A box that is wider than you expected, or a gap that belongs to margin rather than to an empty element, often becomes obvious from the overlay alone before you even read a line of CSS.

There are a few practical details that make the tool smoother. First, inspecting works on elements that only appear on hover or interaction, but those can be tricky: if a dropdown menu disappears the moment you move your mouse towards DevTools, you can use the shortcut to toggle inspect mode and then trigger the element with the keyboard, or pause the page with a breakpoint. You do not need that technique today, but knowing it exists will save a frustrating afternoon later. Second, after you click an element, inspect mode switches off automatically, so you click the icon again for the next element. Third, the element you select becomes $0 in the Console, a connection you will use in Section 14.

Inspecting is also a reading skill, not just a clicking skill. When you select an element, glance at its neighbours in the tree. Is it nested inside the container you expected, or did it end up somewhere surprising because of a missing closing tag earlier in the document? Is it the element you thought, or a wrapper around it? Many layout mysteries resolve at this step, before any CSS is examined, because the structure itself is wrong. A heading inside the wrong section, a list item outside its list, a button sitting outside its form — all of these are visible in the tree the moment you inspect.

Make inspecting a reflex. Whenever a page surprises you, inspect the surprising thing first, then decide what to look at next. That single habit separates developers who debug by staring at code from developers who debug by looking at evidence.

Check: How do you jump from something visible on a page to its element in DevTools?

Section 5 — Editing HTML in the browser

Once an element is selected in the Elements panel, you can change it right there. Double-click its text to edit the words, double-click a tag name to change the tag, or right-click the element for a menu with options like Edit as HTML, which opens the whole chunk of markup for rewriting, Duplicate element, and Delete element. The page updates the instant you confirm the edit, so you can try a different heading, add a missing paragraph, or remove a suspicious element and watch what happens. This is the browser as a sketchpad: fast, reversible, and free of consequences for your real files.

The key thing to understand is that these edits live only in the live DOM. They do not touch your source files on disk, and they do not change anything on the server. Refresh the page and every experiment is gone, replaced by the original markup. That is a feature, not a limitation. It means you can try the boldest possible change — delete half the page, rewrite a section, swap a heading level — and the worst outcome is a reload. Beginners sometimes panic when they realise an edit vanished on refresh; experienced developers rely on it. The rule is simple: experiment in DevTools, and when an experiment works, copy the result into your editor and save it there. DevTools is the rehearsal room, not the stage.

Editing HTML live is useful in several concrete situations. The first is testing a content fix: does this paragraph read better short, does this heading make sense at a different level, does removing this banner improve the layout? You can answer in seconds instead of editing, saving, reloading, and repeating. The second is isolating a problem: if you suspect one element is breaking a layout, delete it in DevTools and see whether the layout heals. If it does, you have found your culprit, and you can investigate that element's styles or structure properly. The third is demonstrating a problem to someone else: you can show a colleague or a client exactly what a change would look like without committing to it.

There are limits worth knowing. You cannot use DevTools editing as a way to change a live website you do not own; your edits affect only your own browser tab. You also cannot easily test changes that depend on a server round-trip, because the edited page is still running the original scripts. And edits to attributes that scripts read only at load time may not take effect until a reload, which would wipe the edit — a circular frustration that tells you the fix belongs in your source, not in the panel. None of these limits reduce the value of the technique; they just mark where live editing ends and real editing begins.

Practice deliberately: open one of your own pages, change a heading's text, add a class to an element, delete a section, and then refresh to watch it all revert. That small exercise teaches the boundary between the live page and your files more clearly than any explanation, and it removes the fear of breaking something by experimenting.

Check: What happens to a change you make in the Elements panel when you refresh the page?

Section 6 — Editing CSS live

Beside the Elements panel sits the Styles pane, and it is where most CSS debugging happens. When you select an element, the Styles pane lists every CSS rule that applies to it, ordered from most specific to least specific, each labelled with the file and line number it came from. Click any property value — a colour, a size, a spacing value — and type a new one, and the page updates instantly. Click an empty space inside a rule to add a new property, or click the checkbox beside a property to switch it off without deleting it. You can also add a brand-new rule for the selected element with the plus button, which creates a rule using the element's selector so you can experiment with properties from scratch.

This live editing transforms how you solve visual problems. Suppose a heading looks too large. In the old way of working, you would guess a smaller size, edit your stylesheet, save, reload, and look. If the guess was wrong, you would repeat the cycle. With live editing, you click the font-size value and press the up or down arrow keys to nudge it, watching the heading shrink or grow in real time until it looks right. Only then do you copy the winning value into your stylesheet. The same technique works for colours, where clicking a colour value opens a colour picker, for spacing, where nudging margin and padding shows the layout breathe, and for layout properties like display and flex settings, where trying a different value costs one click rather than a full edit-and-reload cycle.

h1 {
  color: darkblue;
}

There are two habits that make live CSS editing effective. The first is to experiment boldly and narrow down: try a value far from your guess to see which direction helps, then refine. Small, timid changes teach you little; a big swing shows you the shape of the problem. The second is to treat the Styles pane as a notebook. When you find the combination that works, write it down — either by copying it into your stylesheet immediately or by noting it somewhere — before you refresh or move on, because a refresh erases it. Nothing is more annoying than finding the perfect spacing, losing it to a careless reload, and having to rediscover it.

Live editing also teaches you the cascade in a way reading about it never quite does. You see rules overriding each other, you see which selector actually wins, and you see the effect of toggling a single property off. Over time, that builds an intuition for specificity and inheritance that makes your first guesses in your stylesheet much better. The Styles pane is not just a debugging tool; it is a teacher. Use it every time a page looks wrong, even when you think you already know the fix, and your CSS judgement will improve noticeably within weeks.

Check: Why is editing CSS in DevTools safe to experiment with?

Section 7 — Reading the Styles pane

The Styles pane rewards careful reading. Its rules are listed in order of specificity, with the winning declarations at the top and the overridden ones below, and a declaration that has lost the cascade is shown with a line through it. That strikethrough is information, not decoration: it tells you a rule exists and matches, but something else beat it. Learning to read those lines is the core skill of CSS debugging, because the most common CSS complaint — "I wrote a rule and nothing happened" — is almost always answered by a crossed-out line and the rule above it that won.

Each rule also carries its source: the stylesheet file name and the line number. Clicking that label jumps to the rule in the Sources panel, which is useful when a rule comes from a file you did not expect, such as a framework stylesheet or a browser default. Browser default styles appear under the label "user agent stylesheet", and they are the reason an unstyled page still has margins on the body and large bold headings: the browser applies its own basic rules before yours. Knowing that these exist prevents a common confusion, where a developer hunts through their own stylesheet for a margin that actually comes from the browser.

Inherited properties appear in the Styles pane too, further down, under a heading showing the element they were inherited from. If a paragraph's text is blue and no rule on the paragraph itself sets a colour, the answer is usually an inherited colour from a parent, and the pane shows you exactly which ancestor set it. This matters because inheritance is invisible in your stylesheet: you set a colour on the body, and paragraphs all over the page quietly inherit it. When one of them looks wrong, following the inheritance chain in the pane is far faster than searching your files for a rule that does not exist on that element.

A few more reading skills round out the picture. A warning triangle beside a property usually means the value is invalid — a typo, a missing unit, or a property the browser does not recognise — and the browser has ignored that declaration entirely. An inactive property, greyed out, may be overridden or may simply not apply to that kind of element. And the filter box at the top of the pane lets you search for a property name, so you can ask "what sets this element's width?" without reading every rule. Practice reading the pane on your own pages: pick an element, find its winning colour rule, find one crossed-out rule and identify what beat it, and find one inherited property and trace it to its source. After a few rounds, the cascade stops being an abstract concept and becomes something you can see.

Check: What does a crossed-out rule in the Styles pane mean?

Section 8 — The Computed pane

Next to the Styles pane sits the Computed pane, and it answers a different question. The Styles pane shows the contest: all the rules that applied and which ones won. The Computed pane shows the result: the final value of every property for the selected element, after the cascade, inheritance and browser defaults have all been resolved. If you want to know the actual font size an element ends up with, or its actual colour, or its actual width in pixels, this is where you look. It is the ground truth of how an element is rendered, and when the Styles pane leaves you uncertain, the Computed pane settles it.

The Computed pane is especially valuable for properties that are computed rather than written. A width set as a percentage becomes a pixel value here. A font size set in relative units becomes a pixel value here. A colour written as a named colour or a hex code is shown here in a consistent form. This matters because layout bugs are often about the computed result, not the written rule: an element that is 50 percent wide inside a container that is itself narrower than expected will be narrower than you planned, and only the computed width shows the real number. Similarly, an element whose height is "auto" gets its real height from its content, and the Computed pane tells you what that height turned out to be.

The pane also has a useful interactive feature. Expand a property and it shows the chain of rules that contributed to the final value, from the winning rule down through the overridden ones, each with its source. This is a shortcut to the same investigation the Styles pane offers, but organised around one property at a time. When a single property is the problem — the margin is too large, the colour is wrong, the font is not the one you chose — expanding that property in the Computed pane takes you straight to the winning rule and its rivals, without the noise of every other property.

One caution: the Computed pane shows final values, not intentions. It will not tell you that you meant a different value; it tells you what the browser decided. If the computed value surprises you, the investigation moves back to the Styles pane to find which rule produced it. Think of the two panes as partners: Computed tells you what is, Styles tells you why. Beginners often live entirely in one pane or the other; experienced developers move between them, using Computed to confirm the symptom and Styles to find the cause. When you next face a "why is this element this size?" puzzle, start in Computed, note the number, then chase that number back through Styles.

Check: What is the difference between the Styles pane and the Computed pane?

Section 9 — The box model diagram

Every HTML element is a box, and the box model diagram in DevTools draws that box for the selected element. In the middle is the content: the text or image itself. Around it is padding, the space inside the border. Around that is the border itself. And on the outside is margin, the space that pushes neighbouring elements away. The diagram shows the size of each layer in pixels, and hovering over a layer in the diagram highlights that layer on the page, so you can see exactly where the space is. When spacing looks wrong — a gap you did not expect, a box wider than its container, two elements touching when they should not — this diagram usually shows why within seconds.

The most common surprise the diagram reveals is the difference between margin and padding. Both create space, but padding grows the element's coloured background area, while margin is transparent space outside it. A button with generous padding looks bigger and more clickable; a button with generous margin simply sits further from its neighbours. When a background colour extends further than expected, the culprit is usually padding. When elements are further apart than expected, the culprit is usually margin. The diagram makes the distinction visible instead of theoretical, because you can see which layer holds the unexpected pixels.

Another frequent discovery is margin collapsing, a behaviour where the vertical margins of neighbouring elements combine into a single margin rather than adding up. Two paragraphs each with twenty pixels of margin do not sit forty pixels apart; they sit twenty pixels apart, because the larger margin wins and the smaller one is absorbed. This surprises almost every beginner at least once, and the box model diagram is the fastest way to confirm it is happening: the diagram shows each element's own margin, and comparing the two explains the gap you see. Collapsing only happens vertically and only in certain layouts, which is why it feels inconsistent until you recognise the pattern.

The diagram also helps with sizing puzzles. By default, an element's width sets the width of its content box, and padding and border are added on top, so an element declared at 300 pixels wide with 20 pixels of padding on each side actually occupies 340 pixels. That is why elements sometimes overflow their containers even though their widths look correct. Many developers now set box-sizing to border-box, which makes width include padding and border, and the diagram will reflect whichever model is in effect. Either way, reading the diagram tells you the true occupied size, which is the number that matters for layout.

Make the diagram part of your routine: whenever spacing or sizing surprises you, select the element and read its four layers before changing any CSS. You will often find that the CSS is doing exactly what you wrote, and your mental picture of the box was the thing that was wrong.

Check: Which four layers does the box model diagram show?

Section 10 — The Console panel

The Console panel is the page's voice. It is where the browser prints messages: errors when scripts fail, warnings when something looks off, and any messages your own code prints with console methods. It is also a place where you can type JavaScript and run it against the current page, which makes it both a logbook and a laboratory. If something on a page is broken, the Console is usually the first place that says so, and experienced developers keep it open by default while they work, glancing at it the way a driver glances at a dashboard.

Console messages come in levels, and the levels have colours for a reason. Errors are red, and they mean something failed: a script crashed, a file was not found, a value was used wrongly. Warnings are yellow, and they mean something is odd but the page carried on: a deprecated feature, a questionable value, a resource that loaded slowly. Informational messages are usually uncoloured or blue, and they include your own console.log output. You can filter the Console by level, hiding warnings while you chase an error, or hiding everything except your own logs. You can also filter by text, typing part of a message to find matching lines in a busy log. These filters matter because a real page can produce dozens of messages, and the one you need is rarely the first one you see.

The Console also preserves context that is easy to miss. Each message shows which file and line produced it, and clicking that reference jumps to the spot in the Sources panel. Error messages often include a stack trace, a list of the function calls that led to the error, which you can expand to follow the chain. And messages are grouped and counted: if the same warning fires fifty times, the Console shows it once with a count, rather than flooding the panel. All of this is designed around one goal, which is helping you find the line that matters in a large codebase.

One habit to build early is reading the Console before changing code. When a page misbehaves, open the Console and look. If it is empty, that itself is information: the problem is probably in your HTML or CSS, or in logic that runs without errors, and you should reach for the Elements panel instead. If it is full of red, start with the first error, because later errors are often consequences of the first one — a script that crashed early can cause a cascade of failures downstream. Fix the first error, reload, and see what remains. That discipline, first error first, turns a frightening wall of red into a short, ordered list of jobs.

Check: What two jobs does the Console panel do?

Section 11 — Printing values with console.log

Of all the debugging tools available, console.log is the simplest, the most used, and the most underestimated. It prints a value to the Console so you can see what your code is actually doing, rather than what you believe it is doing. The gap between those two things is where almost every bug lives. A variable you are sure holds a number may hold a string. A function you are sure runs may never be called. A value you are sure is set may still be undefined. Printing it replaces belief with evidence, and that is the whole of debugging in one move.

Using it is straightforward: put the value inside the brackets, and it appears in the Console when that line runs. You can print several values at once, separated by commas, and you can print a label alongside a value to make the output readable when several logs are competing for attention. A bare value in a busy Console can be hard to attribute, so labelling your logs is a habit worth forming early. Instead of logging a number alone, log a short description with it, and the Console becomes a readable story of what your code did, in order.

let total = 3 + 4;
console.log(total); // prints 7

Placement matters as much as the call itself. A log placed before a suspicious line tells you what went into it; a log placed after tells you what came out. When a value changes over time, logging it inside a loop or a function shows you each change in sequence, which often reveals the exact iteration where things go wrong. When a function seems not to run at all, a log as its very first line settles the question instantly: if the message appears, the function ran; if it does not, the problem is in whatever was supposed to call it. That kind of binary test — did this line execute, yes or no — is one of the most powerful uses of logging, and it requires no cleverness at all.

There are limits and manners to observe. Logs left in shipped code clutter the Console for anyone who opens it, and in some cases they can leak information about how a page works, so remove or tidy them before a site goes live. Logs inside code that runs many times a second can flood the Console and slow the page, so for hot code, prefer a breakpoint or a conditional log that only fires when something interesting happens. And remember that logging an object shows its state at the moment you expand it in the Console, not necessarily at the moment it was logged, because objects are stored by reference — a subtlety that can confuse you if an object's properties change after the log line. For values you want frozen in time, log a specific property, or a copy, rather than the whole object.

Check: What does console.log(total) do?

Section 12 — console.warn and console.error

Console.log is not the only way to print a message. Console.warn prints a yellow warning, and console.error prints a red error, and both behave like log in every other respect: they print values, they accept labels, and they appear in the Console in order. The difference is meaning. A warning says "this is odd, but the page carried on". An error says "this went wrong". Using the right level is a way of organising your own thinking as much as organising the Console: when you write a warning, you are saying you noticed something suspicious but not fatal; when you write an error, you are saying a path failed and the user may be affected.

console.warn("This price looks unusual.");
console.error("Could not load the data.");

The practical benefit shows up when you filter. The Console lets you show or hide messages by level, so on a page that prints many routine logs, you can hide everything except errors and warnings to see what needs attention. If your own code uses log for everything, filtering cannot help you, because your important messages are mixed in with the routine ones. If you reserve warn and error for genuinely notable situations, the filter becomes a triage tool: red first, yellow second, everything else when you have time. That is the same discipline as the first-error-first rule from Section 10, applied to your own messages.

There is also a social dimension. Other developers, and your future self, read the Console when something goes wrong. A page whose Console is full of red errors looks broken even if the visible page works, and a page whose Console carries clear warnings looks cared for. Libraries and frameworks follow this convention too: they warn when you use a feature in a way that will stop working in a future version, and they error when something they need is missing. Reading those warnings is often the fastest way to learn that your code depends on something fragile, because the library authors are telling you directly.

Use warnings for situations that deserve a human's attention but do not stop the program: a value that is unusual but usable, a fallback being used because the preferred option failed, a piece of data in an unexpected shape that was handled anyway. Use errors for situations where an operation failed outright: data that could not be loaded, a required element that was not found, a calculation that produced an invalid result. And use plain log for routine tracing while you are actively debugging, with the intention of removing it when the bug is fixed. That three-way split — trace, warn, error — is simple, and it scales from a first script to a large application without changing. Filtering by level is a habit worth building, because it turns a noisy log into a short list of jobs worth doing first.

Check: When would you use console.warn instead of console.log?

Section 13 — Running JavaScript in the Console

The Console is not only a place where messages appear; it is also a place where you can type JavaScript and run it immediately against the current page. Click into the input line at the bottom of the Console, type an expression, and press Enter. The result appears below. Try it with something trivial first, like 2 + 2, and you will see 4. Then try something connected to the page, like document.title, and you will see the page's title. Then try something that changes the page, like document.body.style.backgroundColor = "lightyellow", and watch the page change colour. Nothing you do here touches your files; it all happens in the live page, and a refresh undoes it.

This ability makes the Console a scratchpad for ideas. Before you add a new line to your script, you can test it here and see whether it does what you expect. Before you write a complicated expression, you can build it piece by piece in the Console, checking each piece's result. When you are reading someone else's page and wondering what a particular value holds, you can ask the Console directly. It is the fastest feedback loop in web development: type, Enter, see. No saving, no reloading, no switching windows. For small experiments, it beats editing a file every time.

The Console also has conveniences that reward regular use. Pressing the up arrow recalls your previous commands, so you can rerun or adjust them without retyping. Typing part of a name and pressing Tab completes it, and typing a dot after an object shows a list of its properties, which is a gentle way of discovering what an object can do. Multi-line code can be entered with Shift plus Enter for a new line, so you are not limited to one-liners; a short loop or a small function can be typed and run in place. These conveniences turn the Console from a calculator into a genuine workspace.

There are two cautions. The first is that Console code runs with the page's own scripts and variables, which means it can interfere with them: redefining a variable or calling a function from the Console changes the page's state, and that can confuse a debugging session if you forget you did it. The second is security: never paste code into the Console of a site you do not trust, and never run code someone else tells you to paste there, because Console code runs with the page's full privileges. Browsers now warn about this explicitly. Within your own pages, though, the Console is a safe, fast, and indispensable scratchpad, and the habit of testing an idea there before committing it to a file will improve both your speed and your accuracy.

Check: What happens when you type 2 + 2 into the Console and press Enter?

Section 14 — Inspecting the selected element with $0

The Elements panel and the Console are connected by a small convenience that saves a surprising amount of time. In the Console, $0 refers to the element currently selected in the Elements panel. Select a heading in the Elements panel, switch to the Console, and $0 is that heading. Type $0.textContent and you see its text. Type $0.style.color = "red" and the heading turns red on the page. Type $0 itself and the Console shows the element, which you can expand to inspect its properties. It is a bridge: whatever you are looking at in the tree, you can immediately manipulate or question from the Console without writing a selector to find it again.

$0.textContent;

This is useful in several everyday situations. Suppose you have selected a button and want to know whether it has a particular class, or what its current text is, or what happens when you call one of its methods. Instead of writing document.querySelector with a selector you must get exactly right, you use $0, which is already pointing at the right element. Suppose you want to test a style change that is awkward to make in the Styles pane, like setting several properties at once. A few lines in the Console using $0 can do it. Suppose you are reading a script that acts on an element and you want to check that element's current state. Select it, then ask $0 about it.

There are related helpers worth knowing, though $0 is the one you will use most. $1 refers to the previously selected element, $2 to the one before that, and so on, up to a handful of recent selections. That history lets you compare two elements without reselecting them: select one, select the other, then compare $0 and $1 in the Console. Another helper, $$, works like a shortcut for finding many elements: $$("p") returns all paragraphs on the page, similar to document.querySelectorAll. These helpers exist only in the Console, not in your scripts, so do not use them in files you ship; they are conveniences of the tool, not features of the language.

The deeper lesson of $0 is that DevTools panels are not isolated rooms. They share state: the element you select in one place is available in another, the file and line you click in the Console opens in Sources, and a breakpoint you set in Sources pauses the page you see in Elements. Learning these connections is what turns four separate panels into one workshop. Start with $0, because it is the connection you will use daily, and let it teach you to expect the panels to cooperate. That history also lets you compare two elements quickly, without reselecting either one from the tree.

Check: What does $0 mean in the Console?

Section 15 — The Sources panel

The Sources panel is where you read a page's code files. It shows, in a sidebar, every file the page loaded: its HTML document, its stylesheets, its scripts, and any files those scripts loaded in turn. Click a file and its contents appear in the main area, with line numbers down the side. This is the reading room of DevTools, and it is where serious JavaScript debugging happens, because it is where you can watch code run rather than merely seeing its results.

Reading code in Sources differs from reading it in your editor in one important way: you are reading the code the browser actually received. If your build process changes your files, or a cached copy is being served, or a script is loaded from somewhere unexpected, Sources shows the truth. That is why, when a fix you made does not seem to take effect, checking Sources to confirm the browser has the new version is a worthwhile step before you question your logic. It is also why the Network panel's "Disable cache" option pairs so well with Sources work: together they make sure you are reading and running fresh code.

The Sources panel is also where breakpoints live, and breakpoints are the subject of the next section, so this section focuses on the reading skills that make breakpoints useful. First, you can search across all loaded files with Ctrl or Command plus Shift plus F, typing a function name or a piece of text and seeing every file that contains it. That is invaluable in a page that loads many scripts, where you know what you are looking for but not which file holds it. Second, you can pretty-print minified code with the button that looks like braces, which reformats compressed code into readable lines so you can follow it. Third, you can jump to a specific line or function within a file, which matters when a file is hundreds of lines long and the error message points at line 214.

Finally, Sources shows you code that your editor does not: scripts generated by the page, code injected by extensions, and files fetched from other origins. Being able to see all of it, in one place, is part of what makes DevTools trustworthy as a debugging environment. You are not debugging a theory of the page; you are debugging the page itself, file by file, line by line. Get comfortable opening Sources, finding a file, and reading it. The next sections give you the tools to stop that code mid-run and look around inside it. Reading code in Sources also builds familiarity with files you did not write, which matters because real pages load scripts from many authors. The more comfortable you are opening an unfamiliar file and finding your way around it, the faster every later debugging session goes.

Check: What does the Sources panel show?

Section 16 — Setting a breakpoint

A breakpoint is a marker you put on a line of code that tells the browser to pause when that line is about to run. You set one by opening a file in the Sources panel and clicking its line number. A coloured marker appears, and from then on, whenever the page's execution reaches that line, everything freezes: the page stops responding, the line is highlighted, and DevTools shows you the state of the world at that exact moment. The code has not run the line yet, so you are looking at the moment just before it happens, which is exactly where you want to be when something is about to go wrong.

To use a breakpoint, you need to make the code run. Set the breakpoint, then do whatever triggers that code: click the button, submit the form, scroll to the section, reload the page if the code runs at load time. The page will pause, and DevTools will switch to the Sources panel to show you where it stopped. If the page never pauses, that is itself a finding: the code you marked is not running at all, which means the problem is earlier — the event is not wired up, the function is not called, or the condition guarding it is false. A breakpoint that never fires is evidence, not a failure of the tool.

Breakpoints are better than logging for a particular kind of question: what is true at this moment? A log tells you what one value was when one line ran, but a pause lets you ask anything. You can hover over any variable to see its value, you can type expressions in the Console that run in the paused context, and you can look at the Scope and Call Stack panes to see the full picture. Logging is a snapshot; a breakpoint is a pause that lets you walk around the scene. For bugs where you do not yet know which value to check, pausing beats printing, because you can explore freely once stopped.

There are variations worth knowing. A conditional breakpoint only pauses when an expression you write is true, which is a lifesaver inside a loop that runs a hundred times when only one iteration matters: set the condition to the case you care about, and the page sails past the other ninety-nine. A logpoint is a breakpoint that does not pause at all but prints a message instead, giving you logging without editing your code — handy when you want a trace through code you do not want to modify. And breakpoints persist across reloads in the same tab, so you can set one, reload, and land on it again, which is the normal rhythm of debugging a load-time bug.

One practical warning: a paused page looks broken to a casual observer, because it has stopped responding. If you walk away from a debugging session, resume or remove your breakpoints, or the tab will sit frozen. The resume button, usually a play-like icon, continues execution. Get used to the cycle: pause, look, step or resume, and keep moving. Breakpoints are the most powerful single debugging tool in the browser, and the investment in learning them pays off in every project after this one.

Check: What does a breakpoint make the browser do?

Section 17 — Stepping through code

Once a breakpoint has paused the page, you control time. Three buttons, usually shown as arrows over a line or a dot, let you move forward in small, deliberate steps. Step Over runs the current line and moves to the next line in the same function. If the current line calls another function, Step Over runs that whole function without stopping inside it, and lands on the following line. Step Into does the opposite: if the current line calls a function, it jumps inside that function to its first line, so you can watch what happens there. Step Out finishes the current function immediately and pauses on the line that called it, which is useful when you have seen enough of a function and want to return to its caller.

Choosing between them is a matter of where you suspect the problem. If you trust a function and only care about its result, Step Over it. If you suspect the function itself, Step Into it. If you stepped into a function by accident, or you have understood it and want to leave, Step Out. Most stepping sessions are a mix: over the lines you trust, into the call you doubt, out again when you have your answer. There is no penalty for stepping into the wrong place; Step Out brings you back, and the worst cost is a few clicks.

Stepping is also how you catch bugs that logging misses. Consider a value that changes several times inside a function. A log after the function tells you only the final value. Stepping shows you each change in order, and you can watch for the moment a value becomes wrong. Consider a condition that takes an unexpected branch. A log tells you which branch ran only if you thought to log it. Stepping shows you the branch being taken, live, with the condition's value visible beside it. The difference is between asking the code questions in advance and watching it answer questions you had not thought to ask.

While stepping, keep an eye on the Scope pane, which updates at every step to show the current variables and their values. Watching a variable change as you step is often the moment a bug becomes obvious: a counter that jumps by two instead of one, a string that gains an unexpected character, an object that loses a property. You can also evaluate expressions in the Console while paused, using the current values, to test a theory without moving forward: if this variable held a different value, would the next line work? That combination — stepping plus live evaluation — is the closest thing debugging has to thinking aloud with the program.

A final practical note: stepping through a long function line by line can be tedious, and tedium leads to inattention. Use breakpoints to skip to the interesting part, step only through the lines that matter, and use Step Over aggressively for code you trust. Debugging is not about watching every line; it is about watching the right lines. The skill is knowing which lines those are, and that judgement improves with every session.

Check: What is the difference between Step Over and Step Into?

Section 18 — Watching values while paused

Pausing at a breakpoint is only useful if you can see what is true at that moment, and DevTools gives you several ways to look. The Scope pane, usually shown beside the paused code, lists the variables currently in play, grouped by where they come from: local variables of the current function, variables from enclosing scopes, and global variables. Each shows its current value, and objects can be expanded to reveal their properties. As you step through code, the Scope pane updates, and values that changed since the last step are often highlighted, so your eye is drawn to what just moved. Reading the Scope pane is the first thing to do after any pause, before you touch anything else.

Alongside Scope sits the Watch panel, where you can add expressions to follow across steps. A watch expression is any piece of JavaScript — a variable name, a property, a small calculation — and DevTools re-evaluates it at every pause and shows the result. Watches are useful when the value you care about is not a simple variable: the length of a list, the result of a calculation, a property nested inside an object. They are also useful when the variable you care about is not in the current scope at every step, because a watch keeps showing its value whenever it is available and tells you when it is not. Add a watch by clicking the plus button in the Watch pane and typing the expression.

You can also hover over a variable name directly in the code to see its current value in a small popup. This is the quickest check of all, and it is the one you will use most while stepping, because it requires no panel navigation: your eyes are already on the line, and the value appears beside it. For a deeper look, you can select an expression in the code, right-click, and evaluate it, or type expressions into the Console, which runs in the paused context and can see the same variables the paused code can see. That last point is powerful: while paused, the Console is not the global console of the page, but a window into the paused function's world, so you can test ideas using the function's own variables.

There is a discipline to watching values that is worth naming. When you pause, first form a prediction: what do you expect this variable to hold? Then look. If the value matches your prediction, the bug is elsewhere, and you move on. If it does not match, you have found a lead, and the next question is where the value went wrong, which you answer by stepping backwards in your thinking — or by setting an earlier breakpoint and watching the value from its birth. Debugging is a loop of prediction and observation, and the Scope pane, watches, hovering and the Console are the instruments that let you observe. Use all of them, and trust what they show over what you assumed.

Check: How can you check a variable's value while paused at a breakpoint?

Section 19 — The call stack

Every time a function calls another function, the browser remembers where it came from, so it can return there when the called function finishes. That chain of remembered return points is the call stack, and while paused at a breakpoint, DevTools shows it in the Call Stack pane, with the most recent call on top and the original entry point at the bottom. Reading it from top to bottom answers a question that is often the key to a bug: how did the code get here?

That question matters because many functions are called from several places. A utility function that formats a price might be called from the cart page, the product page and the checkout page. If it fails only sometimes, the difference is usually in who called it and with what arguments. The call stack tells you which caller you are dealing with in this particular pause, and clicking any frame in the stack jumps to that caller's code and shows you its variables at the time of the call. You can walk up the stack, frame by frame, reading each caller's state, until you find the place where a wrong value entered the chain.

The stack also explains a common error message. When a script crashes, the Console's stack trace is the same chain, printed as text: the failing function on top, its caller below, and so on down to the event or script that started it all. Learning to read a stack trace is therefore the same skill as reading the Call Stack pane, and it works even when you were not paused at a breakpoint — the trace is captured at the moment of the error. Start at the top for the failing line, then read downwards for the path that led there. Frames from library code can be skipped; frames from your own code are where your attention belongs.

One more concept belongs here: stack overflow. If a function calls itself forever, or two functions call each other forever, the stack grows without limit until the browser stops it with a "Maximum call stack size exceeded" error. That error is a sign of runaway recursion, a function that never reaches its stopping condition, and the stack trace will show the same function repeated many times. You will meet recursion properly in the chapter on functions, but recognising this error now means you will not be mystified by it later. The call stack is, in short, the program's memory of its own journey, and learning to read it turns "the code crashed somewhere" into "the code came from here, with this value, and that is where it went wrong". A stack trace in the Console is the same chain written out, so learning one teaches you the other.

Check: What question does the call stack answer?

Section 20 — Pausing on exceptions

Sometimes you know a page is throwing an error but you do not know which line is responsible, or the error happens in code you did not write and cannot easily breakpoint. For those cases, DevTools offers pause on exceptions: a button in the Sources panel, usually shaped like a stop sign or a pause symbol, that tells the browser to pause automatically whenever an error is thrown. Turn it on, trigger the bug, and the browser stops on the exact line that failed, with the Scope pane and call stack ready for inspection. You did not need to know where the error was; the browser found it for you.

There are two modes, and the distinction is worth knowing. Pausing on all exceptions stops even on errors that are caught and handled by the page's own code — errors that the page recovers from and that you might not care about. That mode can be noisy on pages that use exceptions as part of their normal flow. Pausing only on uncaught exceptions stops only on errors that escape handling and would appear in the Console, which is usually what you want when chasing a visible bug. The button typically cycles between off, pause on uncaught, and pause on all, so you can choose the level of noise you are willing to tolerate.

Pause on exceptions pairs naturally with the Console. When an error appears in the Console, its message and stack trace tell you what failed, but the moment has passed: the values that caused it are gone. With pause on exceptions enabled and the bug reproduced, you arrive at the failure while it is happening, with the offending values still in scope. You can hover over the variable that is undefined, walk up the call stack to see who passed it, and form a fix based on evidence rather than reconstruction. It is the difference between reading an accident report and arriving at the scene.

A practical workflow follows from this. When a bug report says "the page shows an error" but you cannot see where, enable pause on uncaught exceptions, reproduce the bug, and inspect the pause. If the pause lands in library code, walk up the stack to your own code, because the library is usually reacting to something your code passed it. If the pause lands in a place you recognise, check the values in scope and the line itself. And when you are done, turn the feature off again, or it will keep pausing on every handled error during normal browsing of that tab. Like breakpoints, it is a tool you switch on for a job and off when the job is done. Switch it off when the job is done, so later browsing is not interrupted by pauses.

Check: When is pausing on exceptions more useful than setting a breakpoint by hand?

Section 21 — The debugger statement

Breakpoints are set in DevTools, but there is also a way to put a pause directly in your code: the debugger statement. Writing debugger; on a line of your script tells the browser to pause there whenever DevTools is open and that line runs. It behaves exactly like a breakpoint, except that it travels with the file: anyone who opens the page with DevTools open will pause there, and the pause survives reloads, edits and sharing the file with a colleague. That portability is both its strength and its risk.

function calculate(price) {
  debugger;
  return price * 2;
}

The strength shows up in situations where a breakpoint is hard to set. If the code runs during page load before you can open Sources, a debugger statement is already waiting. If the code is generated or loaded in a way that makes the Sources panel awkward to navigate, the statement finds its own place. If you want to share a debugging starting point with a colleague — "open the page and you will pause right where it goes wrong" — a debugger statement communicates it without instructions about which file and line to click. For those cases, it is a genuinely useful tool, and experienced developers use it without embarrassment.

The risk is forgetting it. A debugger statement left in shipped code will pause the page for any visitor who happens to have DevTools open, which is confusing and unprofessional, and it signals to anyone reading the code that the file was not tidied before release. The rule is therefore strict: debugger statements are for active debugging only, and removing them is part of finishing the job, like removing console.log tracing. Some teams add checks to their build process that refuse to ship code containing debugger statements, which is a sensible safety net, but the primary defence is habit: when a bug is fixed, search your changed files for debugger and for stray logs before you consider the work done.

There is a subtlety about when the statement does anything. If DevTools is closed, debugger; does nothing at all: the page runs straight past it. That is by design, and it means a forgotten statement does not break the page for ordinary visitors. But it also means you cannot use it to debug a page unless DevTools is open, which is a detail that confuses beginners who add the statement, reload without opening the tools, and wonder why nothing paused. Open DevTools first, then trigger the code. The same ordering rule as the Network panel applies here: tools open before the event you want to observe.

Used deliberately and removed promptly, the debugger statement is a small, sharp tool. It is not a replacement for breakpoints, which are easier to add and remove without touching code, but it is a valuable addition for load-time bugs and for sharing a pause point. Treat it like a sticky note on a page of a book: helpful while you are working, and something you peel off before you hand the book to someone else.

Check: What does the debugger statement do, and why should you remove it before shipping?

Section 22 — The Network panel

While the Elements panel shows what a page is and the Console shows what it says, the Network panel shows what a page asks for. Every time a page loads a file — its HTML, its stylesheets, its scripts, its images, its fonts, and any data it fetches — the Network panel records a row for that request. Each row shows the file's name, the type of resource, its size, how long it took, and the status code of the response. Together, the rows tell the story of how a page assembled itself, and when a page is slow, incomplete or broken, that story usually names the culprit.

The most important habit for using the Network panel is opening it before the requests happen. The panel only records requests made while it is open, so if you open it after a page has loaded, it will look empty or incomplete, and you will wrongly conclude that nothing was requested. Open DevTools, switch to the Network panel, and then reload the page. The rows will fill in as the browser fetches each file, in order, and you can watch the page assemble itself file by file. That live filling is itself informative: files that appear late, or that take a long time, stand out.

Reading a row is a skill of its own. The name tells you which file was requested, and a name you do not recognise may be a script or stylesheet you did not know the page loaded — often a third-party addition. The status column tells you whether the request succeeded, and red rows with error codes deserve attention first. The size column tells you how much data came back, and a surprisingly large file is often the reason a page feels slow. The time column tells you how long the request took, and a file that took seconds while others took milliseconds is a lead worth following. Clicking a row opens a detail view showing the request and response headers, a preview of the content, and timing information, which is where deeper investigation happens.

The Network panel also answers questions that no other panel can. Is a stylesheet actually loading, or is the page unstyled because the file is missing? Is a script failing to load, which would explain why a button does nothing? Is an image request returning an error, which would explain a broken image icon? Is the page making far more requests than you expected, perhaps because a script is fetching data in a loop? Each of these is a Network panel question, and each has a visible answer in the rows. When a bug involves something absent — a missing image, a missing style, a missing behaviour — the Network panel is often the fastest way to confirm whether the missing thing was ever delivered.

Check: What does one row of the Network panel represent?

Section 23 — Reading status codes

Every response a server sends carries a status code, a three-digit number that summarises what happened. You do not need to memorise all of them, but a handful cover most debugging. Codes in the 200s mean success: 200 OK is the standard success, and it means the file was found and delivered. Codes in the 300s mean redirection: the browser was told to look elsewhere, which is normal for some resources and usually invisible to you. Codes in the 400s mean the browser asked for something wrong: 404 Not Found means the file does not exist at that address, and 403 Forbidden means the server refuses to serve it. Codes in the 500s mean the server itself had a problem: 500 Internal Server Error is the generic "something went wrong on our side".

In practice, 404 is the code you will meet most, and it almost always means a wrong path. A stylesheet linked as css/style.css when the file actually lives at styles/style.css, an image referenced as photo.jpg when the file is named Photo.jpg on a case-sensitive server, a script whose folder was renamed — all of these produce a 404, a red row in the Network panel, and a page missing that resource. The fix is usually in the reference, not in the file: check the path in your HTML, check the actual file name and folder, and check capitalisation, because servers on Linux treat Photo.jpg and photo.jpg as different files even though your own computer might not.

A 500 error points in the opposite direction: your request was fine, but the server failed while handling it. On a static site, 500s are rare, because there is no server code to fail. On a site with a backend, a 500 means the server-side code or database had a problem, and the fix lives there, not in your front-end files. Knowing which side of that line an error falls on saves you from hunting in the wrong codebase. Similarly, a request that never completes — stuck at pending — usually means a slow server or a network problem, not a bug in your page.

Status codes also appear outside the Network panel: in the Console, a failed request often logs an error mentioning the code, and clicking it jumps to the Network row. Learning the handful of common codes turns those messages from cryptic numbers into plain statements: found, not found, forbidden, server error. That translation is a small piece of knowledge with a long working life, because status codes are the same across the whole web, in every language and framework you will ever touch. Learning those few codes turns a cryptic number into a plain statement about what happened.

Check: What does a 404 status code mean?

Section 24 — Filtering network requests

A real page can make dozens or hundreds of network requests, and the Network panel lists all of them. Finding the one you care about in that list requires filtering, and the panel gives you several ways to narrow it down. The type buttons near the top — All, Doc, CSS, JS, Img, Font, and others depending on the browser — show only requests of that type. Clicking JS hides everything but scripts; clicking Img shows only images. That single click turns a wall of rows into a short, relevant list, and it is the first filter to reach for.

The filter text box is the second tool. Typing part of a file name shows only rows whose name or address contains that text, so typing "logo" shows the logo image and anything else with logo in its name, and typing "api" shows data requests to an address containing that word. You can also filter by properties, using the panel's filter syntax, but name filtering covers most daily needs. Combining the two — type plus text — narrows further: JS plus a name fragment finds one script among many.

A third kind of filtering is hiding the noise the browser itself generates. Requests from extensions, favicon requests, and data URLs can clutter the panel, and most browsers offer options to hide them or to show only requests from the page itself. The "Hide data URLs" and similar options, along with the ability to block specific requests, help you focus on the page's own behaviour rather than the browser's housekeeping. None of these options change the page; they only change what the panel shows you, so experimenting with them is safe.

Filtering is not just convenience; it is a debugging method. When a page loads slowly, filtering to the largest or slowest requests identifies the suspects. When an image is broken, filtering to images shows the failing request at once. When a script misbehaves, filtering to scripts shows whether it loaded and how long it took. The general principle is the same as in every panel: reduce the visible set to the relevant set before you start reading closely. A developer who filters first reads ten rows; a developer who does not reads two hundred. The ten-row developer finishes sooner and with fewer mistakes, because attention is a limited resource and filtering spends it wisely. Filtering also helps when you inherit a page you did not build. A new page may load scripts, styles, fonts and data from sources you do not recognise, and filtering by type lets you inventory each kind separately: scripts first, then styles, then images, checking each group for failures or surprises before moving on. That inventory is often the fastest way to understand an unfamiliar page, because it shows what the page depends on, grouped by kind, rather than as one undifferentiated list of requests.

Check: How do you show only image requests in the Network panel?

Section 25 — Reloading to fill the Network panel

The Network panel has one behaviour that confuses every beginner at least once: it only records requests made while it is open. Open DevTools after a page has finished loading, switch to the Network panel, and you will see an empty list or a partial list, even though the page obviously loaded files. The panel is not broken, and the page is not special; the panel simply was not watching when the requests happened. The fix is to reload the page with the panel open, and the rows will appear as each request is made. This ordering rule — tools open first, event second — is worth memorising, because it applies to the Network panel, to breakpoints on load-time code, and to any observation of something that happens once.

There is a related feature that makes reloads more honest: "Disable cache". Browsers normally cache files, serving repeat visits from a local copy instead of asking the server again. That is good for visitors, but bad for debugging, because a cached file means no network request to inspect, and worse, it can mean you are looking at an old version of a file after you changed it. With "Disable cache" checked while DevTools is open, the browser fetches fresh copies on every reload, so the Network panel shows real requests and you see your latest changes. The setting only applies while DevTools is open, so it does not slow down your normal browsing.

A useful reload variation is the hard reload, available by right-clicking the reload button while DevTools is open, or with a keyboard shortcut. A hard reload bypasses the cache for that reload even without the Disable cache setting, and an "Empty Cache and Hard Reload" option goes further by clearing cached files for the site before reloading. These are the tools to reach for when a change you made is not appearing: before you doubt your code, make sure the browser is actually showing your new code. A surprising number of "my fix did not work" stories end with a cached file.

Finally, remember that the Network panel can be preserved across navigations with the "Preserve log" option, which keeps rows from earlier pages instead of clearing them when you navigate. That is useful when a bug involves a redirect or a form submission that moves to a new page, because otherwise the evidence disappears at the moment of navigation. Like Disable cache, it is a checkbox you set for a job and unset afterwards, so that your normal panel stays tidy. Together, these options — reload with the panel open, disable cache, preserve log when needed — make the Network panel a reliable witness rather than a sometimes-empty list. Keep Preserve log off for ordinary work, so each navigation starts with a clean panel.

Check: Why might the Network panel look empty, and what fixes it?

Section 26 — The timing waterfall

Each row of the Network panel has a small horizontal bar, and together those bars form a waterfall chart. The bar's position shows when the request started, relative to the page load, and its length shows how long the request took. Reading the waterfall is a skill that turns a list of times into a story about why a page is slow. A page with one very long bar has a single slow file; a page with many medium bars has a volume problem; a page whose bars start late has a waiting problem, where the browser knew what it needed but could not ask for it yet.

The bars are also coloured or segmented to show phases of a request, and hovering over a bar reveals a breakdown. The phases typically include queuing (waiting for a free connection), DNS lookup (finding the server's address), connecting and securing the connection, sending the request, waiting for the server's first response, and downloading the content. That breakdown matters because it tells you where the time went. A long waiting phase points at a slow server or a slow server-side process. A long downloading phase points at a large file on a slow connection. A long queuing phase points at too many simultaneous requests competing for connections. Each diagnosis leads to a different fix, and guessing without the breakdown often leads to fixing the wrong thing.

A common waterfall pattern is a chain: one script loads, and only after it finishes does it trigger requests for more files, which trigger more files in turn. The waterfall shows this as a staircase, each step starting where the previous one ends, and the total load time becomes the sum of the chain. Chains are often caused by scripts that load other scripts, or by stylesheets that import other stylesheets. Shortening a chain — by loading independent files in parallel, or by combining files — is one of the most effective performance improvements, and the waterfall is how you spot a chain in the first place.

Another pattern is a single bar that dwarfs all others: one image or script that takes several seconds while everything else finishes in milliseconds. That file deserves attention first, whether the fix is compressing it, resizing it, or questioning whether the page needs it at all. Conversely, a waterfall full of tiny bars that together add up to a slow page suggests the page is asking for too many small files, and combining or removing some of them will help more than optimising any single one.

You do not need to analyse a waterfall every day, but when a page feels slow, the waterfall is where the feeling becomes a measurement. Open the Network panel, reload, and look at the shape before you look at any number. The shape tells you the kind of problem; the numbers tell you its size. That order — shape first, numbers second — keeps performance work focused on the real bottleneck instead of the most obvious file.

Check: What does a long bar in the network waterfall tell you?

Section 27 — The Application panel and storage

Web pages can store data in the visitor's browser, and the Application panel is where you inspect that storage. It shows, for the current site, its local storage and session storage (key-value stores that scripts use to remember things), its cookies (small pieces of data the server and the page use to recognise a returning visitor), its cached files, and any databases the site uses. Each store can be expanded to show its keys and values, and you can edit or delete individual entries, or clear a whole store. This makes the Application panel the place to go when a page behaves differently for a returning visitor than for a first-time one.

That difference is more common than beginners expect. A site might remember a preference, a login, a shopping cart, or a dismissed banner in storage, and then behave according to that remembered value. If the stored value is stale or corrupt, the page can break in ways that are invisible in the code: the code is correct, but it is reading bad data. Clearing storage and reloading is therefore a classic diagnostic step, equivalent to asking "does this happen for a brand-new visitor?" If clearing storage fixes the problem, the bug lives in the stored data or in how the page handles it, not in the page's current logic. If clearing storage changes nothing, you have ruled out a whole category of causes with one action.

Storage also matters for privacy and for testing. A developer testing a login flow needs to clear cookies and storage between tests, or each test starts already logged in and proves nothing. A developer building a feature that saves a user's preference needs to watch the stored value change as the feature is used, confirming that the right key is written with the right value. The Application panel lets you watch that happen live: use the feature, and the stored entry appears or changes. Without the panel, you would be guessing whether the save worked.

There are a few cautions. Clearing storage for a site logs you out of it and removes its saved preferences, which is fine on your own test pages but mildly annoying on sites you use daily, so be deliberate about which site's storage you clear. Storage is also scoped per origin, meaning one site cannot read another site's storage, which is a security boundary worth understanding: your page's data is yours, and other pages cannot peek at it. And storage is not unlimited; browsers set quotas, and a page that stores too much will fail to save, which the panel can help you notice by showing how much is stored.

For everyday debugging, the Application panel is a specialist tool rather than a daily one. You will not open it as often as Elements or Console, but when a bug smells like remembered state — it works in a private window but not in a normal one, it works for others but not for you, it worked yesterday and not today without a code change — the Application panel is where you look first, and it will often settle the question in under a minute.

Check: What kind of data can you inspect in the Application panel?

Section 28 — Testing small screens with device mode

Most visitors will meet your pages on phones, so testing at phone sizes is not optional, but owning every phone is impractical. Device mode solves this: it is the phone-and-tablet icon in the top-left of DevTools, and when you switch it on, the page renders inside your desktop browser at the size of a chosen device. You can pick from a list of common phones and tablets, or set a custom width and height, or drag the edges of the rendered page to resize it freely. The page reflows as you resize, so you can watch a layout break and heal in real time as the width crosses the points where your design changes.

Device mode does more than shrink the page. It can simulate touch instead of mouse input, so hover-dependent menus reveal their mobile behaviour. It can simulate a device's pixel ratio, so images and text render at the sharpness a real phone would show. It can spoof the user-agent string, so a server that serves different content to phones will serve the phone version. And it can simulate network and location conditions alongside the size. These simulations are approximations, not perfect replicas, but they catch the large majority of responsive problems: text that overflows, buttons that are too small to tap, navigation that does not fit, images that are wider than the screen.

The most effective way to use device mode is to resize slowly and watch. Start wide, drag narrower, and note the widths where things break: a heading that wraps badly, a row of cards that squeezes instead of stacking, a form that overflows its container. Each breaking width is a clue about a missing or misplaced breakpoint in your CSS, and fixing it there improves the page for every device near that width, not just the one you were simulating. Then check a few specific device sizes from the list, especially a small phone and a tablet, to confirm the fixes hold at real-world widths.

There are limits, and honesty about them matters. Device mode cannot reproduce a real phone's performance, its exact browser quirks, or the feel of touch on a small screen. A page that is smooth in device mode can still stutter on a low-end phone, and a gesture that works with a mouse in device mode can be awkward with a thumb. That is why Section 39 covers remote debugging on a real device: device mode is the daily tool, and real-device testing is the periodic check that catches what simulation misses. Together they cover responsive testing well. Use device mode every time you build a layout, and use a real phone before you ship anything important. Rotate between portrait and landscape too, since a layout can work in one orientation and fail in the other.

Check: How do you preview a page at a phone's width without a phone?

Section 29 — Simulating a slow network

Your internet connection is probably faster than many of your visitors'. A page that loads instantly for you can take painful seconds for someone on a mobile network, a shared connection, or a rural link, and you will never know unless you test under those conditions. The Network panel lets you throttle the connection: a dropdown, usually set to "No throttling", offers presets like "Slow 3G" and "Fast 3G", and sometimes a custom option. Choose a preset, reload the page, and the browser artificially slows its requests to match that connection. The page you know well suddenly shows its true behaviour for a large part of the world.

What you learn from throttling is often surprising. Parts of the page you assumed appear together may arrive seconds apart: the text first, then the styles, then the images, then the interactive scripts. A page that depends on a large script may sit unresponsive for a long time on a slow connection, even though it feels instant on yours. A loading spinner that you barely notice on fast internet may dominate the slow-connection experience. An image-heavy hero section may push the actual content far down the load order. Each of these observations points at a concrete improvement: smaller files, fewer files, better loading order, or a design that works before everything has arrived.

Throttling also pairs with the waterfall from Section 26. On a throttled reload, the bars stretch out, and the order in which things appear becomes clearer. Files that block rendering — typically stylesheets in the head and scripts that must run before the page can be shown — stand out because everything waits for them. Files that are merely slow but non-blocking are less urgent. That distinction, blocking versus non-blocking, is the key to performance work, and throttling makes it visible in a way that fast connections hide.

A note on fairness: throttling simulates network speed, not device speed. A slow phone with a fast connection can still struggle, because its processor renders and runs scripts more slowly, and throttling will not show that. Performance testing therefore has two halves: network throttling for connection problems, and real-device testing for processor problems. Both matter, and neither replaces the other. But throttling is free, instant, and available in every debugging session, so there is no excuse for never trying it. Before you ship a page, reload it once on Slow 3G and watch it load the way a meaningful share of your visitors will. If the experience embarrasses you, fix it before they meet it. Throttling also reveals whether a page shows anything useful before its scripts arrive. A page that stays blank for seconds on a slow connection is telling you its content depends too heavily on code that arrives late, and restructuring it so text appears first is a genuine improvement for every visitor, not just those on slow networks.

Check: Why would you deliberately slow down your own connection while testing?

Section 30 — The Performance panel in brief

The Performance panel answers a question the Network panel cannot: once the files have arrived, why does the page still feel slow? It records what the browser is doing over a period of a few seconds — running JavaScript, calculating styles, laying out elements, painting pixels — and draws a timeline of that activity. You start a recording, interact with the page or let it load, stop the recording, and study the timeline. Long stretches of scripting, long layout calculations, and repeated painting all show up as blocks you can click to investigate, with details about which function or element was involved.

You do not need the Performance panel every day. For most bugs, the Network panel, the Console and breakpoints are enough, and a performance recording is overkill. Reach for it when a page feels sluggish but the Network panel looks healthy: files arrive quickly, no errors appear, yet scrolling stutters, animations jerk, or clicks respond late. That pattern points at work happening on the page itself — a script running too often, a layout that is recalculated on every scroll event, an animation that forces the browser to repaint large areas repeatedly. The timeline makes that work visible and measurable, which is the first step to reducing it.

Reading a recording takes practice, and the panel offers summaries to help. A flame chart shows which functions consumed time, with wider blocks meaning more time, so the widest blocks are the first suspects. The summary view breaks time into categories — scripting, rendering, painting, and idle — and a page dominated by scripting needs different fixes from one dominated by painting. Screenshots captured during the recording show what the user saw at each moment, which connects the timeline to the experience: a long blank stretch before content appears is a different problem from a page that appears quickly but responds slowly.

The honest scope of this section is an introduction, not a mastery course. Performance analysis is a deep field, and this book returns to performance in a later chapter with more detail. For now, the takeaway is that the panel exists, that it records browser activity rather than network activity, and that it is the tool for "the files are fine but the page feels slow". Knowing which tool answers which question is most of the skill. When you meet a sluggish page with a clean Network panel, you will know where to look next instead of guessing. A recording is also useful evidence when discussing performance with others. Saying a page feels slow is vague, but showing a timeline where one function consumes most of the time turns a feeling into a measurement, and measurements can be acted on, prioritised, and re-checked after a fix to confirm improvement.

Check: When is the Performance panel the right tool instead of the Network panel?

Section 31 — Reading an error message properly

An error message is not an insult and it is not noise; it is the browser's best attempt to tell you exactly what went wrong, and learning to read it fully is one of the highest-value skills in this chapter. A typical Console error has three parts. The first is a description of the problem, in words: what the browser tried to do and why it could not. The second is a location: the file and line number where the problem occurred, which you can click to jump to that spot in the Sources panel. The third is a stack trace: the chain of function calls that led to that line, which you can expand to follow the path the code took. Many beginners read only the first part, or only the first line, and then start guessing. Reading all three parts takes seconds longer and often removes the need to guess at all.

The description deserves slow reading, because it usually names the exact value or operation involved. "Cannot read properties of undefined (reading 'length')" is not a vague complaint; it says the code tried to read a property called length from something that was undefined. That sentence contains a diagnosis: find the thing that was undefined, and find why it was undefined, and the bug is understood. Similarly, "is not a function" says the code tried to call something that is not callable — often a variable that holds a value instead of a function, or a misspelled method name. The habit to build is to repeat the description in your own words before touching any code: what did the browser try, and what was missing? If you cannot restate it, you have not understood it yet, and changing code at that point is guessing.

The location is the second gift, and it is worth trusting even when it surprises you. The failing line is not always the line you need to fix — the real cause may be earlier, where a wrong value was created — but the failing line is where the wrongness became visible, and it is the starting point for walking backwards. Click it, read the surrounding lines, and ask what each value on that line holds. The stack trace then extends that walk: each frame is a caller, and each caller had its own values. Follow the chain until you find the frame where a value first went wrong, and you have found the neighbourhood of the fix.

Finally, resist the urge to search the internet for the error message before reading it yourself. Searching is sometimes necessary, especially for errors from libraries, but a search that skips reading produces copy-pasted fixes that may not fit your situation. Read the message, read the line, read the stack, form a theory, and only then search if the theory needs support. Error messages are written by people who wanted you to understand the problem. Take them up on the offer.

Check: What three parts does a typical Console error give you?

Section 32 — Three kinds of errors

Not all errors are the same kind, and knowing which kind you are facing changes how you hunt it. The three kinds are syntax errors, runtime errors, and logic errors, and they differ in when they appear and how loudly they announce themselves.

A syntax error means the code cannot even be read. A missing bracket, a misspelled keyword, a string that was never closed — the browser's parser hits the mistake and stops, refusing to run the script at all. Syntax errors are loud: they appear in the Console immediately, with a location, and often the whole script fails rather than just one feature. They are also usually quick to fix once found, because the parser points at or near the offending character. The main difficulty is that the reported location can be slightly after the real mistake — a missing closing bracket early in a file may only be noticed at the end — so when a syntax error's location looks innocent, look backwards for the unclosed something.

A runtime error means the code is valid and starts running, but crashes part-way through. Using a variable that does not exist, reading a property of undefined, calling something that is not a function — these are runtime errors. They appear in the Console when the failing line executes, which may be immediately at load or only when a user clicks a particular button. Unlike syntax errors, a runtime error does not necessarily kill the whole script: code before the failing line ran, and code after it may never run, which can produce the confusing symptom of a page that half works. Runtime errors are the kind breakpoints and pause-on-exceptions are built for, because arriving at the failing line with its values visible usually explains the crash.

A logic error is the quiet one. The code runs without any error message and produces the wrong answer: a total that is too high, a list that is missing its last item, a condition that sends users down the wrong path. Nothing points at it, because from the browser's point of view nothing went wrong — the code did exactly what it said, and what it said was not what you meant. Logic errors are found by testing against expectations: you know what the answer should be, you observe what it is, and you narrow the gap with logs, breakpoints and stepping until you find the line where the program's idea of the world diverges from yours. They are the hardest kind, and also the most common in real work, because they survive every automated check that only looks for crashes.

Knowing the kind tells you the tool. Syntax error: read the message and look backwards. Runtime error: pause at the failure and inspect values. Logic error: compare expected against actual, then bisect the code until the divergence appears. That mapping turns "something is wrong" into a plan.

Check: Which kind of error gives no error message at all?

Section 33 — "Cannot read properties of undefined"

If there is one error message every JavaScript developer learns by heart, it is this one: "Cannot read properties of undefined (reading 'x')", where x is the property the code tried to read. Older browsers phrase it as "Cannot read property 'x' of undefined", but the meaning is identical. The code expected something to exist, and it did not. A variable that was supposed to hold an object holds undefined instead, and the code tried to use it as if it were an object. The error is the browser refusing to pretend.

let heading = document.querySelector(".missing");
console.log(heading); // null, if no such element exists

The most common cause is a failed lookup. Document.querySelector returns null when no element matches the selector, and reading a property of null produces a closely related error about null rather than undefined. Either way, the pattern is the same: the code assumed a lookup succeeded, and it did not, usually because of a misspelled class name, a selector that does not match the HTML, or a script that runs before the element exists in the document. The fix is rarely at the failing line itself; it is in the selector, the timing, or the HTML. Check the selector against the actual markup, check that the script runs after the document is ready, and check that the element really is on the page.

Other causes follow the same shape. A function that sometimes returns nothing — because a code path forgot a return statement — gives its caller undefined, and the caller then fails when it uses the result. Data fetched from a server may be missing a field you assumed would always be present, so a property access on that missing field fails. A variable may be declared but never assigned on the path that actually ran, because the assignment sits inside a condition that was false. In each case, the debugging move is identical: log the value just before the failing line, see that it is undefined or null, and then ask where it was supposed to get its value and why it did not.

Defensive habits reduce these errors, but they do not replace understanding. You can guard a lookup by checking whether the result exists before using it, and you can use optional chaining — a question mark before the dot — to say "if this is missing, stop here and give me undefined instead of crashing". Those techniques are useful, and a later chapter covers them. But a guard that silently hides a missing element can also hide a real bug, so use guards for genuinely optional values and fix the cause for values that should always exist. The error message, annoying as it feels, is doing you a favour: it is telling you that an assumption failed. Believe it.

Check: What does "Cannot read properties of undefined" usually mean?

Section 34 — ReferenceError: a name is not defined

A ReferenceError announces itself plainly: "ReferenceError: someName is not defined", where someName is the exact word the browser could not resolve. Your code used a name — a variable, a function — that the browser does not know in that scope at that moment. The message names the word, which is a gift: you do not have to guess which name is the problem, because the browser just told you. Your job is to find out why that name is unknown, and the causes fall into a short list.

The most common cause is a typo. A variable declared as userName and used as username, or as usernmae, is a different name to JavaScript, which is case-sensitive and exact. The fix is spelling, and the way to find it is to compare the declaration with the use, letter by letter. The second cause is scope: the name exists, but not where you used it. A variable declared inside a function is not visible outside that function, and a variable declared inside a block is not visible outside that block, which is the subject of the chapter on scope. If the name is declared in one place and used in another, check whether the use sits inside the region where the declaration is visible.

The third cause is order: the name is declared, but the line that uses it runs before the declaration line. For let and const, using the name before its declaration throws a ReferenceError about the temporal dead zone, which you met in the chapter on values and variables. For function declarations, hoisting usually saves you, but for variables it does not, and the fix is to move the declaration above the use or to restructure the code so the use happens later. The fourth cause is a missing declaration altogether: a name used without ever being declared, perhaps because a line was deleted or because you assumed a library had defined it. In that case the fix is to declare it, or to load the library that provides it.

A disciplined response to a ReferenceError is therefore a short checklist: read the name in the message, find its declaration, compare spelling, check scope, check order. Work through that list before changing anything, and the fix usually presents itself. What you should not do is declare a second variable with the same name in a new place to silence the error, because that creates two variables where there should be one and invites a subtler bug later. Fix the name, the scope or the order, not the message. When you find the cause, fix the spelling, the scope, or the order, and re-run the code to confirm the error is gone before moving on.

Check: What is the most common cause of a ReferenceError?

Section 35 — When the page looks wrong but there is no error

CSS problems rarely produce Console errors, and that silence confuses beginners who have learned to equate "broken" with "red message". A layout can be badly wrong — overlapping text, a collapsed container, a button in the wrong place — while the Console sits empty, because from the browser's point of view nothing failed. The browser applied your CSS exactly as written; the result simply is not what you wanted. Invalid CSS is ignored silently rather than reported as an error, and valid CSS that produces an unwanted layout is, to the browser, a success. Debugging layout is therefore a different activity from debugging scripts, and it uses different tools.

The primary tool is the Elements panel with its Styles pane and box model diagram, and the method is reading rather than guessing. Select the element that looks wrong. Read its box model: is the content the size you expected, is the padding what you wrote, is a margin creating the gap you see? Read its Styles pane: which rules apply, which are crossed out, which property holds the surprising value? Read the Computed pane for the final numbers. In most layout bugs, one of those readings reveals the cause within a minute: a margin you forgot, a width that includes padding you did not account for, a rule from a framework overriding yours, a flex or grid setting on a parent that changes how children behave.

There are a few classic silent causes worth memorising. An element with no height because its children are floated or positioned can collapse to nothing, making content below it jump upward. An element wider than its container because of padding added to a fixed width can overflow and push siblings aside. A missing unit — writing width: 100 instead of width: 100px — makes the declaration invalid, so the browser ignores it entirely, and the element falls back to its default size. A typo in a property name, like colr instead of color, is likewise ignored silently. And a selector that does not match anything applies to nothing, silently, which is why a rule that "does nothing" usually means the selector is wrong, not the property.

The mindset shift is this: for scripts, the browser tells you when something fails; for styles, you must go and look. That is not a flaw in the tools; it is the nature of a language where almost any combination of rules is technically valid. Get comfortable with the reading routine — inspect, read the box, read the styles, read the computed values — and silent layout bugs become as tractable as noisy script errors. The evidence is there; it is just in the Elements panel rather than the Console. That reading routine is slower than glancing at the Console, but it is the routine that finds silent bugs.

Check: Why does a broken layout usually come with no error message?

Section 36 — Debugging a rule that loses

"I wrote a rule and nothing happened" is the most common CSS complaint, and it has a short list of causes that you can work through in order instead of changing values at random. The first check is whether the selector matches the element at all. A selector with a misspelled class, a wrong tag name, or a descendant relationship that does not exist in the markup will match nothing, and the rule will sit in your stylesheet doing nothing. Verify the match in DevTools: select the element, look at the Styles pane, and see whether your rule appears there at all. If it does not appear, the selector is wrong or the stylesheet is not loading, and no change to the property value will help.

The second check is whether another rule is overriding yours. If your rule appears in the Styles pane with a line through its declaration, something more specific, or later in the cascade with equal specificity, has beaten it. The pane shows the winning rule directly above, with its source file and line number, so you can go and look at it. The fix depends on the situation: sometimes you genuinely need a more specific selector; sometimes the winning rule is a mistake that should be changed; sometimes the winning rule comes from a framework or a browser default and your rule needs to be written to account for it. What you should avoid is escalating specificity wildly — piling on ids and !important declarations — because that creates a stylesheet where every new rule must fight harder than the last, and the whole file becomes brittle. Fix the cascade, do not outmuscle it.

The third check is whether the stylesheet itself is loading. A rule cannot apply if its file never arrived, and the Network panel will show a 404 or a failed request for the missing file. This happens with wrong paths, renamed folders, and case-sensitive file names, as Section 23 described. It also happens when a stylesheet link tag has a typo in its href, or when a build step fails to copy the file. Checking the Network panel takes seconds and rules out a whole category of causes. A related variant is caching: the stylesheet loaded, but it is an old cached copy without your new rule, and the fix is a hard reload with cache disabled, as Section 25 described.

Work through the three checks in order — match, override, loading — and you will find the cause of almost every "nothing happened" rule. The order matters because it moves from the most local cause (your selector) to the most global (the file delivery), and each check either finds the problem or eliminates a category cleanly. Random changes, by contrast, can appear to fix a problem while actually adding a new one, or can fix it in a way you cannot explain and therefore cannot repeat. Debugging is the discipline of eliminating causes in order, and this three-check list is that discipline applied to CSS.

Check: List the three checks for a CSS rule that does not apply.

Section 37 — Errors versus warnings

The Console colours its messages for a reason, and learning to triage by colour is a daily skill. Errors, in red, mean something failed: a script crashed, a request failed, a value was used wrongly. Warnings, in yellow, mean something is odd but the page carried on: a feature is deprecated and will be removed in a future browser version, a value was interpreted in a surprising way, a resource loaded but not in the ideal form. Informational messages, including your own logs, are routine. The triage rule is simple: fix errors first, then warnings, then tidy logs. Errors are user-visible breakage or imminent breakage; warnings are future breakage or sloppiness; logs are your own working notes.

The reason errors come first is not just severity but causality. A page that throws an early error often throws later errors as consequences: a script that crashed before defining a function causes errors everywhere that function is called, and a failed data load causes errors in every piece of code that expected the data. Fixing the first error can make a dozen later errors vanish, which is why the first-error-first rule from Section 10 is so effective. Chasing a later error before the first one is fixed is wasted effort, because the later error may not exist once the first is resolved. Reload after each fix and recount: the list usually shrinks faster than you expect.

Warnings deserve attention second, not never. A deprecation warning is a message from the future: the feature you are using still works today, but a browser update will remove it, and your page will break on that day without any change from you. Fixing deprecations promptly is cheap; fixing them after removal, under pressure, is not. Other warnings point at real quality problems: a large image served at a small size, a stylesheet that blocks rendering, a security setting that is weaker than recommended. None of these stop the page today, but each is a small debt, and debts compound. A page whose Console is clean of warnings is a page whose author is paying attention.

There is also a professional dimension. Anyone who opens your page's Console — a colleague, a client, a reviewer — judges the page partly by what they find there. A Console full of red errors looks broken even if the visible page works, because it suggests the author does not look. A Console with clear, intentional warnings looks cared for. Keeping the Console clean is therefore not vanity; it is part of shipping quality work, like keeping a workshop tidy. Make a habit of glancing at the Console before you consider any piece of work finished, and treat a new warning as a task, not as background noise.

Check: Should you fix errors or warnings first, and why?

Section 38 — Source maps

When a site is shipped to visitors, its JavaScript and CSS are often minified: whitespace removed, names shortened, files combined, all to make downloads smaller and faster. Minified code is efficient for the network and miserable for a human to read. A single line can hold an entire file's logic, variable names are single letters, and line numbers bear no relation to the original source. Debugging minified code directly is possible but painful, and for a long time it was simply the price of shipping optimised code. Source maps remove that price.

A source map is a separate file that maps the minified code back to the original source: which original file, which original line, which original variable name corresponds to each piece of the minified output. When DevTools loads a script that has a source map, it uses the map to show you the original files and line numbers in the Sources panel, even though the browser actually downloaded the minified version. Breakpoints set in the original code pause at the right place in the minified code. Error stack traces show original locations. The debugging experience becomes almost identical to debugging unminified code, which is the whole point: you ship small files and debug readable ones.

Source maps are produced by build tools — the bundlers and minifiers that a later chapter covers — and they are usually referenced by a comment at the end of the minified file pointing at the map file. You do not need to create them by hand, and you do not need to understand their format. What you need to know is that they exist, that they are what makes production debugging sane, and that when they are missing, DevTools falls back to showing minified code, which is a signal that the build is not configured to produce them. Some teams choose not to ship source maps publicly, because a map reveals original source code to anyone who looks, and for those teams production debugging happens with maps kept private. That is a legitimate trade-off, and it is a decision for a later stage of your career.

For your own learning, the practical point is smaller: if you open a live site's Sources panel and see readable, multi-file code, source maps are doing their job. If you see one enormous line of compressed code, they are absent, and the pretty-print button can make it readable enough for a quick look. Either way, the panel is showing you the truth of what the browser received. Source maps are one more example of the theme of this chapter: the browser gives you tools to see past the surface, and understanding what those tools do makes you a more effective developer even before you need them daily.

Check: What does a source map let DevTools show you?

Section 39 — Debugging on a real phone

Device mode is an excellent daily tool, but it is a simulation, and simulations have limits. A real phone has a real processor, often slower than your computer's, a real browser with its own quirks, a real network that fluctuates, and a real touchscreen operated by thumbs rather than a precise mouse pointer. A page that is smooth in device mode can stutter on a low-end phone. A gesture that works with a mouse can be awkward with a thumb. A feature that works in your desktop browser can behave differently in a phone's browser. Real-device testing catches what simulation misses, and for anything important, it is not optional.

Remote debugging connects a real phone to your desktop DevTools. The usual setup is a phone connected to the computer by cable, with USB debugging enabled on the phone and the computer's browser configured to discover it. Once connected, the phone's browser tabs appear in your desktop DevTools, and choosing one opens a DevTools session that inspects the phone's page: its Elements panel shows the phone's DOM, its Console shows the phone's messages, its Sources panel lets you set breakpoints in the phone's code. You interact with the phone itself — tapping, scrolling, typing — while watching the desktop panels respond. It is device mode with the simulation removed.

The workflow is worth practising before you need it under pressure. Enable developer options on the phone, enable USB debugging, connect the cable, accept the connection prompt on the phone, and find the device in your desktop browser's remote devices view. Each of those steps has its own small pitfalls — a cable that only charges, a prompt dismissed by accident, a driver missing on the computer — and working through them once, calmly, means you can repeat the setup quickly later. Wireless remote debugging is also possible in some setups, which removes the cable but adds network configuration, so start wired.

Real-device testing is also a test of empathy. Using your own page on a small screen, with your thumb, on a connection like your visitors', teaches you things no panel can: that a button is too close to another button, that a form field is hard to reach, that a page that loads in two seconds on your computer takes eight on a phone. Those lessons change how you design, not just how you debug. Device mode is for every day; a real phone is for every release. Build both into your routine, and your pages will be genuinely usable, not just theoretically responsive. Testing on a real phone also means testing with real hands: tap targets that look fine with a mouse pointer may be too small or too close together for a thumb, and only using the page yourself, on the device, reveals that.

Check: What does remote debugging let you do that device mode cannot fully do?

Section 40 — console.table for lists of data

When a value is a single number or string, console.log prints it plainly and that is enough. When a value is a list of objects — users, products, search results, rows of data — console.log prints a collapsed list that you must expand item by item to read, which is slow and makes comparison hard. Console.table solves this by printing the list as a table, with one row per item and one column per property, so you can scan, sort by eye, and spot anomalies at a glance. It is a small method with an outsized effect on how quickly you can inspect data.

let users = [{ name: "Ada", age: 30 }, { name: "Ben", age: 25 }];
console.table(users);

The table shows each object's properties as columns, which immediately reveals inconsistencies: a missing value shows as an empty cell, a value of the wrong type stands out against its column neighbours, a duplicate row is visible as a repeated line. When data comes from a server, these are exactly the problems you check for, and a table shows them faster than expanding twenty objects one by one. You can also pass a second argument listing which columns to show, which is useful when objects have many properties and you care about only two or three. That focus — show me names and ages, hide everything else — turns a wide, noisy table into a precise answer to a question.

Console.table also works for a single object, showing its properties as rows, which can be clearer than the collapsed object view for objects with many properties. And it works in combination with the filtering and grouping habits from earlier sections: filter a list to the interesting items, then table the result, and you have a readable report rather than a pile of logs. Like console.warn and console.error, it is a matter of choosing the right display for the data you have, and the right display saves time twice: once when you read it, and once when you avoid misreading it.

A caution carried over from console.log applies here too: the table shows objects by reference, so expanding a row shows the object's current state, not necessarily its state at logging time. For data you want frozen, table a copy or table specific properties rather than the live objects. And as with all console methods, remove data-dumping tables before shipping, both for tidiness and because data printed to the Console is visible to anyone who opens it. Used during development, though, console.table is one of those habits that, once adopted, feels obvious in retrospect: of course a list of similar things should be shown as a table. Tables are also easier to share: a screenshot of a table communicates a data problem to a colleague faster than a collapsed list.

Check: Why use console.table instead of console.log for a list of objects?

Section 41 — Reproduce the bug first

Before you change a single line to fix a bug, make the bug happen again on purpose, and write down the exact steps. Open the page, perform the actions, and watch the failure occur. Note what you clicked, what you typed, what the page showed, and what the Console said. This sounds like delay when you are eager to fix things, but it is the foundation of every reliable fix, for one simple reason: if you cannot reproduce a bug reliably, you cannot tell whether your fix worked. A bug that disappears after your change may have been fixed, or it may simply have hidden — because the data was different, the timing was different, or the step you skipped mattered. Reproduction turns "it seems fixed" into "it is fixed, and here is the proof".

Reproduction also sharpens the bug report itself. Vague reports — "the form is broken" — become precise ones: "when I enter a price above 1000 and click Save, the total shows NaN". Precision matters because it narrows the search: the bug involves prices above 1000, the Save action, and the total calculation. Each detail eliminates code that cannot be involved. A bug you can reproduce on demand is a bug whose boundaries you understand, and understanding boundaries is most of debugging. Conversely, a bug you cannot reproduce is a bug you do not yet understand, and fixing it by changing code at random is likely to either miss it or break something else.

There is a practical discipline to reproduction. Reduce the steps to the smallest set that still triggers the bug, removing anything unnecessary. If the bug needs ten steps, find out whether it needs all ten or whether three suffice. The smaller the reproduction, the faster each test cycle, and the clearer the connection between the steps and the failure. Record the steps somewhere durable — in the bug report, in a comment, in a message to yourself — because you will need them again after the fix, to verify, and possibly again later, if the bug returns. A recorded reproduction is an asset; a reproduction that lives only in your head is lost the moment you get distracted.

Finally, reproduction protects you from a subtle trap: fixing the wrong bug. Pages often have several problems at once, and without a clear reproduction you can fix one, see improvement, and declare victory while the reported bug remains. With a reproduction, you have a test: run the steps, observe the failure, apply the fix, run the steps again, observe the success. That loop — reproduce, fix, re-run — is the basic unit of trustworthy debugging, and every technique in this chapter slots into it. Breakpoints, logs, and the Network panel are all ways of understanding a reproduction; the reproduction itself is what makes their findings meaningful.

Check: Why should you reproduce a bug before trying to fix it?

Section 42 — Divide and conquer

When you do not know where a bug lives, the worst strategy is reading the entire codebase from the top, hoping the bug announces itself. The best strategy is divide and conquer: split the suspect code in half, disable or bypass one half, and see whether the bug survives. If it survives, it lives in the half you kept. If it vanishes, it lives in the half you removed. Either way, you have halved the search area with one test. Repeat, and a file of hundreds of lines narrows to a handful in a few rounds, because halving is exponential: ten rounds can search a thousand lines, and you rarely need ten.

The technique works with several kinds of "halving". You can comment out half of a function and see whether the symptom changes. You can remove half of a page's elements in DevTools and see whether a layout bug heals. You can disable half of a page's scripts and see whether a script error disappears. You can test with half the data, or with a feature switched off. The exact mechanism varies, but the logic is identical: change the size of the suspect area, observe, and let the observation tell you which half to keep. It is the same logic as guessing a number by asking "higher or lower", and it is fast for the same reason: each answer removes half the possibilities.

Divide and conquer also pairs well with the reproduction from Section 41. A reliable reproduction gives you a fast test — run the steps, bug or no bug — and divide and conquer gives you a way to use that test efficiently. Without a reproduction, each halving requires a slow, uncertain manual check. With one, each halving is a quick run of known steps. The two techniques together turn an intimidating bug in a large codebase into a short series of decisive experiments, which is a much calmer experience than staring at code and hoping for insight.

There are two cautions. First, when you disable code, disable it cleanly and reversibly: comment it out, do not delete it, and keep track of what you disabled, or you will fix the bug and then struggle to restore the working code. Version control helps here, because you can always return to the last saved state. Second, remember that some bugs live in the interaction between halves, not in either half alone — two pieces of code that are each correct but conflict when combined. If halving gives confusing results, where the bug vanishes whichever half you remove, suspect an interaction, and investigate how the halves communicate. Even then, divide and conquer has taught you something valuable: the bug is in the boundary, not in either side alone.

Check: How does commenting out half the suspect code help find a bug?

Section 43 — Check your assumptions

Most bugs are not caused by exotic language features or by computers malfunctioning. They are caused by something the developer assumed that is not true. The developer assumed an element exists, but the selector is wrong. Assumed a value is a number, but it arrived as a string. Assumed a function ran, but the event was never wired up. Assumed data arrived in a certain shape, but the server sent something slightly different. Assumed a condition was true, but it was false on the path that actually ran. Each of these is a reasonable assumption, and each is wrong in the specific case, and the bug is the gap between the assumption and reality.

The antidote is to replace assumptions with evidence, one check at a time. That sounds abstract, but in practice it is concrete: print the value, inspect the element, read the Network row, check the condition. When a piece of code misbehaves, list what it assumes — this element exists, this variable holds a number, this function was called, this data has this field — and test each assumption in turn. The first assumption that fails is usually the bug, or points directly at it. This is why console.log is so powerful despite its simplicity: it is an assumption-testing machine. Every log line is a question — "is this value what I think?" — and the Console answers it.

A useful exercise is to practise stating your assumptions out loud, or in writing, before you debug. "I assume the button has the class I selected, the script runs after the page loads, and the total starts at zero." Writing them down makes them testable, and it often reveals an assumption you did not know you were making. The classic example is timing: code that assumes an element exists because the script tag sits below it in the file, when in fact the script is deferred, or the element is created later by another script. Stating the assumption — "the element exists when this runs" — invites the test, and the test settles it.

There is a humility in this approach that serves developers well over a career. The code is not lying to you; it is doing what you wrote. The browser is not malfunctioning; it is following its rules. When those two facts are accepted, the only remaining explanation is that your model of the situation is incomplete, and the job becomes completing the model. That is a calmer and more productive frame than frustration, and it is also more accurate. Bugs are not personal failures. They are places where your understanding and the program's behaviour diverge, and debugging is the work of closing that divergence with evidence. Write the assumption down before you test it, so the test has a clear question to answer.

Check: What is usually behind a bug: bad luck, or a wrong assumption?

Section 44 — Make a minimal reproduction

A minimal reproduction is the smallest possible example that still shows a bug: a few lines of HTML, a few lines of CSS or script, and nothing else, in a fresh file, that fails in the same way as the real page. Building one is work, and beginners often resist it, asking why they should rebuild a bug they already have. The answer is that a minimal reproduction does three valuable things at once. First, it proves the bug is what you think it is, by stripping away everything that is not involved. Second, it makes the bug fast to test, because each experiment runs in seconds on a tiny file instead of in a large application. Third, it makes the bug communicable: you can show it to a colleague, or ask about it online, without requiring anyone to understand your whole project.

The process of minimising is itself diagnostic. Start with a copy of the failing page, and remove pieces one at a time, testing after each removal to see whether the bug survives. Remove a section: does the bug remain? Remove a stylesheet: does it remain? Remove a script: does it remain? Each removal that preserves the bug eliminates a suspect. When you can remove nothing further without the bug disappearing, what remains is the core of the problem, and it is usually small enough to understand at a glance. Often the last removal reveals the cause: the bug disappears when you remove a particular element or rule, and that element or rule is the culprit, or interacts with the culprit.

If minimising from the full page feels daunting, build up instead. Start with an empty file and add pieces until the bug appears. The last piece added is the trigger. Both directions — stripping down and building up — converge on the same small example, and you can use whichever feels easier for the bug in hand. The result is the same: a tiny, self-contained demonstration that anyone can run and anyone can understand.

A minimal reproduction also protects your time in a less obvious way. When you ask for help with a large, complex page, helpers must first understand your page before they can help with your bug, and many will not invest that time. When you ask with a minimal reproduction, the bug is visible immediately, and help arrives faster and more accurately. Even if you never ask anyone, the discipline of minimising often solves the bug before you finish, because the act of isolating it forces you to understand it. It is one of the highest-return habits in debugging, and it costs only the patience to remove things one at a time. Keep the reproduction file after the fix, as proof that the bug existed and that it is gone.

Check: What is a minimal reproduction, and why is it useful?

Section 45 — Change one thing at a time

When you have a theory about a fix, the temptation is to apply several changes at once: fix the selector, adjust the style, and restructure the function, all before testing. Resist it. Change one thing, then test. If the bug is fixed, you know which change fixed it. If it is not, you know that change was not the fix, and you can revert it cleanly. If you change five things at once and the bug disappears, you do not know which change mattered, and you may have introduced a new problem that will only surface later, disguised as an unrelated bug. Single changes produce knowledge; multiple changes produce confusion that happens to look like success.

This rule is the same discipline as a scientific experiment, and for the same reason: a test with several variables cannot attribute its result. In debugging, attribution matters because it determines what you do next. If a selector fix solved the problem, you are done. If a style change masked it while the real cause remains, you are not done, and the bug will return in a different form. Single-change testing also makes your work reversible: each change is small enough to undo without disturbing the rest, which matters when a promising fix turns out to be wrong. Version control amplifies this, because each small change can be a separate commit, and a wrong commit can be reverted cleanly.

The rule also applies to the diagnostic phase, not just to fixes. When you are testing a theory — "is this element the problem?" — change one thing about the test at a time, so the result is interpretable. And it applies to performance work, where changing several optimisations at once makes it impossible to say which one helped. Patience here is not slowness; it is speed in disguise. A developer who changes one thing at a time and tests after each change often finishes sooner than a developer who changes five things and then spends an hour untangling which change broke something else.

There is one legitimate exception, and it is worth naming so the rule does not become rigid. When several changes are inseparable — renaming a variable everywhere it appears, for example — they are really one change, and they should be applied together. The test is whether the changes make sense independently. If each change is a separate idea, test it separately. If they are facets of a single idea, test them as one. That judgement, applied consistently, keeps debugging honest without making it pedantic, and it is a habit that scales from a first script to a large codebase worked on by a team. If a single change does not fix the bug, revert it before trying the next idea, so changes do not pile up.

Check: Why is changing five things at once a bad way to fix a bug?

Section 46 — Using version control to find a bug

"It worked yesterday and it is broken today" is one of the most useful bug reports you can receive, because it contains a powerful clue: something changed between yesterday and today, and the bug is almost certainly in that change. Version control turns that clue into an investigation. If you save your work in small, frequent commits — a habit a later chapter covers in full — you can compare the working version with the broken one and see exactly which lines changed. The bug lives in those lines, or in their interaction with the rest of the code, and the search area shrinks from the whole project to a single commit's worth of changes.

The practical workflow is straightforward. First, confirm the report: check out or recall the last version that worked, and verify that it does work. Then look at what changed since: the list of commits, and within them, the changed lines. If there is one commit, read it. If there are several, test the versions between them to find the first broken one — a process called bisecting, which version control tools can automate by repeatedly checking out the middle commit and asking you whether it works. Bisecting is divide and conquer applied to history, and like divide and conquer on code, it narrows exponentially: a handful of tests can search dozens of commits.

Version control also protects the fix itself. When you find the offending change, you can revert just that change without disturbing later good work, or you can write a targeted fix on top of it, and either way you have a record of what was done and why. That record matters when the bug returns or when a colleague asks why a line looks the way it does. A fix without a record is a mystery for the next person; a fix with a commit message explaining the bug, the cause and the change is documentation.

None of this works without the habit that enables it: committing small, frequent, well-described changes. A project with one giant commit per week gives you a week's worth of changes to search, which is better than nothing but far worse than a project with several commits per day, each describing one idea. The debugging benefit is one more reason to commit often, alongside the safety benefit of never losing work. Developers sometimes think of version control as insurance against losing files; it is also, and equally, a debugging tool, a history book that lets you ask "what changed, and when did it break?" and get a precise answer. Commit messages matter here too: a message that names the bug and the fix turns history into documentation for the next person.

Check: How can version control help when something that worked yesterday is broken today?

Section 47 — Different browsers, different behaviour

A page can work perfectly in one browser and break in another, and that is not a sign that one browser is broken or that you did something wrong. Browsers are separate programs, built by separate teams, implementing a shared set of standards with their own interpretations, their own release schedules, and their own histories. Most of the time they agree. Sometimes they do not, and the differences cluster in predictable places: newer features that one browser has implemented and another has not yet, older features with long-standing quirks, and areas where the standard leaves room for interpretation, like some details of form controls and scrolling.

When a bug appears in only one browser, the first check is whether the feature you used is supported there. The browser's own DevTools are the place to look: an unrecognised property in the Styles pane, an error in the Console about an unsupported method, or a feature that simply does nothing are all signs of a support gap. Reference sites that track feature support across browsers can confirm it, and they will usually also tell you which versions gained support and whether a fallback exists. If support is the issue, the fix is to use a supported alternative or to add a fallback for the browsers that lack the feature, rather than to blame the browser or to rewrite unrelated code.

The second check is whether the bug is really browser-specific or merely data-specific: does it happen in that browser for all users, or only for one user whose extensions, settings or stored data differ? Testing in a private window, with extensions disabled, separates browser behaviour from extension interference, and testing on another machine with the same browser separates the browser from the environment. These eliminations are quick and they prevent a common waste of time: building a browser-specific workaround for a problem that was actually caused by an extension.

A broader habit follows from all of this: test in more than one browser during development, not only at the end. If you develop entirely in one browser and test others only before release, browser differences arrive as a pile of surprises at the worst moment. If you check a second browser regularly — even briefly, even just for the pages you changed — differences arrive one at a time, while the relevant code is fresh in your mind. That habit, plus the knowledge that differences are normal and investigable rather than mysterious, is what makes cross-browser work manageable rather than dreaded. Keep notes on the differences you find, because the same browser quirk tends to reappear in later projects, and a note saves rediscovery. A short note naming the browser, the feature, and the workaround is enough, and it compounds in value as your list of known differences grows.

Check: What should you check first when a bug appears in only one browser?

Section 48 — DevTools in other browsers

Chrome, Edge, Firefox and Safari all ship developer tools, and while each has its own personality, the core panels are the same: Elements (called Inspector in Firefox), Console, Sources (called Debugger in Firefox), and Network. The buttons sit in slightly different places, some features have different names, and some panels have extras that others lack, but the ideas transfer completely. An element inspector is an element inspector, a breakpoint is a breakpoint, and a network row is a network row, whatever the browser calls them. Learning the ideas once is the investment; learning a new browser's layout is a short adjustment.

That transferability matters because you will not use one browser forever. You may develop in one browser and debug a report from another. You may switch jobs and find a team standardised on a different browser. You may need to debug a problem that only appears in a particular browser, which means using that browser's tools whether or not they are your favourite. A developer who understands what a panel is for can open an unfamiliar DevTools, find the equivalent panel by its purpose, and get to work within minutes. A developer who only memorised button positions in one browser must start over.

There are genuine differences worth knowing about, though none of them change the fundamentals. Firefox's tools have historically been strong on layout inspection, with excellent grid and flexbox visualisations. Safari's tools are the only way to debug pages on Apple phones, which makes them essential for that platform despite being less familiar to many developers. Edge's tools are closely related to Chrome's, because both browsers share the same underlying engine, so skills transfer almost directly between them. Chrome's tools are the most widely documented, which makes them a good default for learning. None of these differences is a reason to learn only one set of tools; each is a reason to be comfortable moving between them.

The practical advice is to spend most of your time in one browser's tools, getting fluent, and to occasionally open another browser's tools on the same page and find the equivalent panels. That small exercise builds the transfer skill before you need it under pressure. Debugging is a way of thinking — reproduce, observe, narrow, test — and the panels are instruments for that thinking. Instruments vary, but the thinking does not, and a developer who owns the thinking can pick up any instrument. If you switch browsers for a project, give yourself a day to learn the new layout before judging it. The panels are the same in purpose, but muscle memory takes a little while to transfer, and judging a tool in its first hour usually says more about unfamiliarity than about the tool itself.

Check: Do you need to relearn debugging from scratch for each browser?

Section 49 — The debugging mindset

Tools matter, but the mindset matters more, because tools in the hands of a frustrated guesser produce frustrated guessing faster. Good debuggers share a few attitudes that are worth naming explicitly, because they can be practised like any other skill. The first is calm. A bug is not a personal failure and it is not a judgement on your ability; it is a puzzle with an answer, and the browser gives you instruments to find it. Frustration usually signals that you are guessing instead of testing, and the remedy is to stop, state what you know, state what you assume, and test one assumption. Calm is not a personality trait here; it is a procedure.

The second attitude is curiosity over blame. When code misbehaves, the interesting question is not "who wrote this badly" but "what is this code actually doing, and why does it make sense for it to do that?" Code that looks stupid often turns out to be reasonable given a constraint you did not know about, or to be correct code operating on wrong data. Approaching it with curiosity leads you to the data, the timing, or the assumption that explains the behaviour, while approaching it with blame leads you to rewrite working code and introduce new bugs. This applies to your own code as much as to others': the version of you who wrote the bug had a reason, and finding that reason is faster than condemning the code.

The third attitude is systematic patience. Debugging rewards the developer who works through a list — reproduce, narrow, check assumptions, change one thing, verify — over the developer who tries the first idea that comes to mind and then the second and then the third. The systematic developer looks slower for the first ten minutes and finishes sooner, because their experiments produce knowledge while random attempts produce noise. Keeping notes during a long debugging session — what you tried, what you observed, what you ruled out — extends this patience, because it prevents you from repeating a test you already ran or from losing a clue you already found.

Finally, know when to step away or ask for help. A bug that resists an hour of systematic work is often solved in minutes by a fresh pair of eyes, or by your own eyes after a break, because a rested mind notices the assumption a tired mind kept skipping. Asking for help with a minimal reproduction and a clear description of what you tried is not a failure; it is a professional skill, and it respects the helper's time. The debugging mindset, in short, is curiosity, system, patience, and the humility to ask. Tools amplify it, but they cannot replace it.

Check: What should you do when you feel stuck guessing at a bug?

Section 50 — Chapter 15 in one paragraph

Developer tools turn the browser from a viewer into a workshop: inspect the live DOM and styles in the Elements panel, read and run code in the Console and Sources panels, watch network requests in the Network panel, and simulate phones and slow connections when you need to see the page as your visitors will. Around those tools sits a method that matters more than any single panel: reproduce the bug first, narrow it down by dividing and conquering, replace assumptions with evidence from logs and breakpoints, change one thing at a time, and verify the fix by running the reproduction again. Different browsers have different tools, but the panels share purposes and the method transfers everywhere, and the mindset behind it — calm, curious, systematic — is what turns a frightening error message into a solvable puzzle. Keep DevTools open while you work, keep the Console clean, and let the browser show you what your code really does, because that evidence is the shortest route from a broken page to a working one. That method, practised on small bugs, scales to large ones without changing its shape. Reproduce, narrow, test, verify: the same four steps, whatever the size of the problem. Look back over the chapter and notice how the tools form a workflow rather than a collection. A visual problem starts in the Elements panel, where you identify the element and read its styles and box model. A behavioural problem starts in the Console, where errors and logs tell you what happened, and continues in Sources, where breakpoints and stepping show you why. A loading problem starts in the Network panel, where status codes and the waterfall show you what arrived, what failed, and what was slow. A device problem starts in device mode or on a real phone, where the page meets the conditions its visitors actually use. Each panel answers a different question, and learning which question to ask first is what makes debugging fast: the right panel gives you evidence in seconds, while the wrong panel gives you noise. That workflow is worth rehearsing until it is automatic, because under pressure you will reach for the tool you know best rather than the tool the problem needs, and a rehearsed workflow corrects that instinct. The second half of the chapter, the method, is what turns tool use into reliable fixing. Reproduce before you fix, so you have a test. Narrow by dividing, so each experiment halves the search. Replace assumptions with evidence, so you fix causes rather than symptoms. Change one thing at a time, so you know what worked. Verify by re-running the reproduction, so a fix is proven rather than hoped for. Tools without method produce fast guessing; method without tools produces slow reasoning; together they produce the steady, evidence-based debugging that professional work depends on, and that this chapter has tried to make habitual.

Check: In your own words, name the four core DevTools panels and one job each does.

Practical task

Pick a page you have built earlier in this book and deliberately break it in three ways: misspell a CSS class name, remove a closing tag, and add a JavaScript line that uses a variable that does not exist. Then, using only DevTools, find and describe each break: use the Elements panel and Styles pane for the class name, the Elements panel for the missing tag, and the Console for the JavaScript error. Write down the error or clue for each one, fix all three in your real files, and reload to confirm the page is whole again. Then go one step further: set a breakpoint in your script, reload the page, and step through the fixed code once, watching at least one variable's value in the Scope pane, so that breakpoints become a lived experience rather than a read one.

Chapter questions

  1. A page's button does nothing when clicked, and the Console is empty. Which DevTools panels would you check, in what order, and why?
  2. Explain the difference between the live DOM shown in the Elements panel and the HTML file sent by the server.
  3. A stylesheet rule is crossed out in the Styles pane. What does that mean, and what are two reasons it might be crossed out?
  4. Write the steps you would take to find out why an image on a page is not loading.
  5. A script works when you step through it slowly with breakpoints but fails at full speed. What might that suggest, and how would you investigate?