# Debugging in the Browser

> Browser DevTools without the overwhelm: the Console, breakpoints in the Sources panel, and the Network tab solve most frontend mysteries.


---

# Debugging in the Browser

The page is broken. A button does nothing, the data won't load, a section is the wrong color - and you're
staring at the screen with no idea where to even look. Most people respond by scattering `console.log`
everywhere and refreshing, hoping a clue falls out. There's a calmer way: the browser already ships a full
debugging toolkit (the DevTools), and three of its panels answer almost every "why is this broken?" you'll
ever have.

This guide isn't a button tour of one browser. It teaches the four panels you actually use - Console,
Sources, Network, Elements - as a way of *thinking*, so the moves carry over whether you're in Chrome,
Edge, Firefox, or Safari.

## How to read this

- **New to DevTools?** Read in order. Phase 1 gives you the map and the Console; Phase 2 teaches real
  breakpoints and the Network tab; Phase 3 walks a full investigation start to finish.
- **Already comfortable in the Console?** Skip to [Phase 2](02-breakpoints-and-the-network-tab.md) - that's
  where breakpoints beat scattered logging.

## The phases

1. **[The DevTools Map and the Console](01-the-devtools-map-and-the-console.md)** - the mental model: DevTools
   is a window into the *running* page. Open it, read errors, log and live-evaluate in the Console.
2. **[Breakpoints and the Network Tab](02-breakpoints-and-the-network-tab.md)** - pause your JavaScript with
   breakpoints (step over / into, watch expressions, the call stack) instead of scattering logs, and use the
   Network tab to find the request that failed.
3. **[A Real Investigation](03-a-real-investigation.md)** - walk one "why is this broken?" bug end to end
   across Console, Network, Sources, and Elements, plus the gotchas that send people chasing ghosts.

Related reading: [Reading a Stack Trace](/guides/reading-a-stack-trace) and
[What an Error Message Tells You](/guides/what-an-error-message-tells-you) - the Console throws both at you,
and they're far less scary once you can read them.


---

# The DevTools Map and the Console

Here's the thing nobody tells you when you first press F12: DevTools isn't a separate app that *looks at*
your page. It's plugged *into* the live page - same JavaScript, same memory, same network. Opening it means
climbing inside the running machine while it's still running. Every panel is a different window onto that
one live thing - which is what tells you what each panel is *for*, and turns a wall of tabs into a toolbox
where each tool answers a specific question.

## Opening it (and the one habit that saves you)

You open DevTools with **F12**, or **Ctrl+Shift+I** (Windows/Linux) / **Cmd+Option+I** (Mac), or by
right-clicking anything on the page and choosing **Inspect**.

```text
Right-click the broken thing → "Inspect"
```
*What just happened:* DevTools opened with the **Elements** panel already pointed at the element you
right-clicked - the fastest way in when something *looks* wrong. You land on the culprit instead of hunting for it.

📝 **The habit:** something's broken? *Open DevTools and look at the Console* before you guess or edit code.
Half the time the answer is already sitting there in red.

## The panel map

Lots of tabs across the top - you'll spend ~95% of your time in four. Here's the map, and the one question
each answers:

```text
┌──────────────────────────────────────────────────────────────────┐
│ Elements │ Console │ Sources │ Network │ Performance │ ...          │
├──────────────────────────────────────────────────────────────────┤
│                                                                    │
│  Elements  → "What is the page actually made of, right now?"       │
│              The live HTML + the CSS the browser really applied.   │
│                                                                    │
│  Console   → "Did the JavaScript complain? Let me ask it things."  │
│              Errors, your logs, and a live prompt to run code.     │
│                                                                    │
│  Sources   → "Let me pause the code and step through it."          │
│              Your JS files + the debugger (breakpoints).           │
│                                                                    │
│  Network   → "What did the page ask the server for, and what       │
│              came back?" Every request, status, payload, timing.   │
│                                                                    │
└──────────────────────────────────────────────────────────────────┘
```
*What just happened:* Now you have a mental index. "Page looks wrong" → Elements. "Code threw or I want to
test something" → Console. "Code runs but does the wrong thing" → Sources. "Data won't load" → Network. This
guide covers them in that order.

## The Console: your home base

The Console does three jobs. Get these three and you've got the most useful panel in the browser.

### Job 1: It shows you errors - read them, don't fear them

When JavaScript breaks, it shouts here in red. The instinct is to flinch and scroll past - don't. The error
is usually *telling you exactly what's wrong.*

```console
❌ Uncaught TypeError: Cannot read properties of undefined (reading 'name')
       at renderUser (app.js:42)
       at app.js:108
```
*What just happened:* This isn't noise. It says: something was `undefined`, and the code tried to read
`.name` off it, on **line 42 of app.js**, inside `renderUser`. You already know the *what* (a value you
expected was missing) and the *where* (app.js:42). Click that blue `app.js:42` and DevTools jumps you
straight to the line. Reading these well is its own skill - see
[What an Error Message Tells You](/guides/what-an-error-message-tells-you) and
[Reading a Stack Trace](/guides/reading-a-stack-trace).

⚠️ **Gotcha - read the FIRST error, not the last.** One real bug often triggers a cascade of follow-on
errors. Scroll to the earliest red line; the ones below are frequently dominoes that fell after the first.

### Job 2: It shows you YOUR logs

`console.log` prints whatever you hand it, right here - the simplest debugging tool there is, genuinely
useful for a quick "is this code even running? what's this value?"

```console
> console.log("got here", user)
got here ▸ {id: 7, name: undefined, email: "a@b.com"}
```
*What just happened:* You printed a label and an object. Note `name: undefined` - there's the root of that
TypeError above. The `▸` triangle means the object is expandable: click it to drill into nested fields. Bonus
tip: `console.table(arr)` renders an array of objects as a real table, far easier to scan than expanded blobs.

### Job 3: It's a live prompt - ask the running page questions

This is the part beginners miss, and it's the Console's superpower. That `>` prompt runs JavaScript *inside
your live page, right now.* Read any variable, call any function, poke at the real state - no added code, no
refresh.

```console
> document.querySelectorAll(".cart-item").length
3
> user.email
"a@b.com"
> 1500 * 0.0825          // sanity-check a calculation
123.75
```
*What just happened:* You interrogated the live page three ways - counted real elements on screen, read a
real variable's value, and used the Console as a calculator. No edits, no refresh. That's the shift from
*guessing* to *asking.*

💡 **Key point:** `console.log` *tells* the page to report something on its next run. The live prompt *asks*
the page, at its current state, right now. The second is faster and you'll lean on it constantly once it clicks.

## For builders

Wire a clear label into your logs from day one - `console.log("checkout: total before tax", total)` beats a
bare `console.log(total)` you can't find among twenty others. And strip `console.log` lines before you
ship: they leak internal state and clutter real users' consoles (most build setups can do this for you).

## Recap

1. DevTools is a live window into the *running* page, not a separate viewer - that's why each panel answers a
   different question.
2. First move on any bug: **open DevTools, look at the Console.** Often the answer is already there in red.
3. The Console does three jobs: shows **errors** (read the first one, click the file link), shows **your
   logs** (`console.log`, `console.table`), and gives you a **live prompt** to run code against the real page.

Next up: when a log isn't enough and you need to *pause* the code and watch it run - plus the panel that
solves "the data won't load."

```quiz
[
  {
    "q": "Something on the page is broken. What's the recommended first move?",
    "choices": [
      "Add console.log statements throughout the code and refresh",
      "Open DevTools and read the Console before changing anything",
      "Restart the dev server",
      "Clear the browser cache and try again"
    ],
    "answer": 1,
    "explain": "DevTools is a live window into the running page. Looking at the Console first often reveals the error immediately, before you waste time guessing or editing code."
  },
  {
    "q": "The Console shows a stack of red errors. Which one should you read first?",
    "choices": [
      "The last (bottom) one - it's the most recent",
      "The longest one - it has the most detail",
      "The first (earliest) one - the rest are often dominoes that fell after it",
      "It doesn't matter; they're all the same bug"
    ],
    "answer": 2,
    "explain": "One real bug often triggers a cascade of follow-on errors. Scroll up to the earliest red line; the later ones are frequently consequences of the first."
  },
  {
    "q": "What makes the Console's `>` prompt different from a console.log statement?",
    "choices": [
      "Nothing - they do exactly the same thing",
      "The prompt only works on the Elements panel",
      "The prompt runs code against the live page right now, while console.log reports on the next run",
      "console.log can read variables but the prompt cannot"
    ],
    "answer": 2,
    "explain": "The live prompt executes JavaScript inside the running page at its current state, so you can ask it questions (read variables, call functions) without adding code and refreshing."
  }
]
```


---

# Breakpoints and the Network Tab

`console.log` only tells you what you *thought to print*, after the fact - a value wrong mid-function, a loop
misbehaving on iteration nine, and you're stuck in a log-refresh-read loop. This phase covers two better
tools: **Sources**, for pausing code instead of guessing what to print, and **Network**, for the single most
common frontend bug class - "the data won't load."

## Breakpoints: freeze the code and look

A breakpoint marks a line and tells the browser: *stop right before it runs and hand me control.* The page
runs full speed until it hits that line, then **freezes** - now you see every variable's real value at that
instant. No printing, no guessing. You look.

Set one in **Sources**: open your JS file, click the line number in the left margin, and a blue marker appears.

```text
Sources panel - checkout.js

  39   function applyDiscount(cart, code) {
  40     const subtotal = cart.total;
● 41     const rate = DISCOUNTS[code];        ◄── breakpoint set here
  42     return subtotal - subtotal * rate;
  43   }
```
*What just happened:* Next time `applyDiscount` runs, it pauses *before* line 41 - `cart`, `subtotal`, and
`code` already hold their real values, `rate` isn't computed yet. You're standing inside the function, mid-run.

When paused, DevTools shows three things:

```text
┌─ PAUSED on checkout.js:41 ──────────────────────────────────────┐
│                                                                 │
│  SCOPE (what's true right now)      CALL STACK (how we got here)│
│    code     = "SUMMER"                ▶ applyDiscount  :41       │
│    subtotal = 80                        handleCheckout :120      │
│    cart     = {total: 80, items: 3}     onClick        :12       │
│                                                                 │
│  WATCH                              CONTROLS                     │
│    DISCOUNTS[code]  = undefined       ▷ resume   ⤼ step over     │
│    subtotal * 0.2   = 16              ↓ step into ↥ step out     │
└─────────────────────────────────────────────────────────────────┘
```
*What just happened:* Watch shows `DISCOUNTS[code]` is `undefined` - `"SUMMER"` isn't in the `DISCOUNTS`
object. Bug found without a single `console.log`: the lookup returns nothing, so the math produces garbage.

### The controls you actually use

Once paused, drive the page forward one piece at a time:

- **Resume** (▷) - run until the next breakpoint (or the end).
- **Step over** (⤼) - run the current line whole, *including any function it calls*, stop on the next line.
  Use when you trust what a line calls and only want the result.
- **Step into** (↓) - descend into a called function and pause on its first line - follow a bug into a helper.
- **Step out** (↥) - finish the current function, pop back to the caller. Use when the bug wasn't in what you
  stepped into.

```text
# Paused at: return subtotal - subtotal * rate   (line 42, rate = undefined)
> step over
# Page resumes... returns NaN. There's the symptom: undefined rate → NaN total.
```
*What just happened:* Stepping over line 42 with `rate` as `undefined` produced `NaN` (`80 - 80 * undefined`).
Bug traced from cause (`DISCOUNTS["SUMMER"]` missing) to symptom (`NaN` price) - a complete diagnosis.

### Watch expressions and the call stack

The **Watch** panel holds expressions you pin, re-evaluated *every time the page pauses* - add
`DISCOUNTS[code]` once and see its value at every stop, no re-typing.

The **call stack** is the chain of calls that got you here: `onClick` called `handleCheckout` called
`applyDiscount`. Click any frame to jump into *that* function with *its* variables - no re-running needed.
See [Reading a Stack Trace](/guides/reading-a-stack-trace) if frames feel shaky.

💡 **Why this beats scattering logs:** `console.log` shows one guessed value; a breakpoint shows *every*
value, lets you step at your own pace, and answers questions you didn't anticipate.

⚠️ **Gotcha - your code might be minified.** In a built app, `checkout.js` may arrive as one unreadable line.
Check for **source maps**: if your build emits them (most dev setups do), DevTools shows the original source
for breakpoints. Minified soup means they aren't loading - fix that first.

## The Network tab: when the data won't load

The other giant bug class isn't in your code - it's in the conversation between page and server. The
**Network** tab records every request the page makes.

Open Network, **then reload the page** (it only records while open), and you get a list:

```text
Name              Status   Type    Size    Time
─────────────────────────────────────────────────
index.html        200      doc     4.2 kB   80 ms
app.js            200      script  120 kB   40 ms
GET /api/user     200      fetch   1.1 kB   95 ms
GET /api/orders   500      fetch   612 B   210 ms   ◄── red row
```
*What just happened:* One row is red: `GET /api/orders` came back **500** - the server errored fulfilling the
request. The order list is empty because the data never arrived, not because *your* code is broken.

### Reading a status code at a glance

The status code tells you who's at fault, roughly:

```text
2xx  → it worked.            200 OK, 201 Created
3xx  → redirect.             301, 302
4xx  → YOU asked wrong.      400 bad request, 401 unauthorized,
                             403 forbidden, 404 not found
5xx  → the SERVER broke.     500 internal error, 502, 503
```
*What just happened:* `4xx` means *your request* was wrong (bad URL, missing auth token, malformed body);
`5xx` means *the server* fell over - one digit aims you at the right half of the system.

### Click a request to see everything

Click any row and a detail pane opens with tabs - where the real answers live:

- **Headers** - full URL, method, status, request/response headers (confirms an auth token was actually sent).
- **Payload/Request** - what your code *sent*: did you POST the fields the server expected?
- **Response/Preview** - what came *back*: often the actual server error for a `500`, or the real data shape
  for a `200` with wrong data.
- **Timing** - how long each phase took - catches "it's not broken, it's slow."

```text
GET /api/orders  →  Response tab:
  { "error": "column \"user_id\" does not exist" }
```
*What just happened:* The Response body handed you the real cause - a wrong database column name on the
server. No guessing what the backend did; a bug report you can hand off with confidence.

## For builders

Check Network *before* your own code when a feature "doesn't work" - half the time the request failed and
your code is fine. And use **Preview/Response** to confirm an API returns the shape your code expects: a
`200` with wrong JSON keys breaks the UI as thoroughly as a `500`.

## Recap

1. A **breakpoint** (Sources panel, click the line number) freezes the page before a line runs and shows you
   every local value - no `console.log`, no refresh-and-guess loop.
2. **Step over / into / out** drive the paused code forward; **watch expressions** re-check on every pause;
   the **call stack** shows how you got there.
3. The **Network tab** records every request - reload with it open. The **status code** points the finger
   (`4xx` = your request, `5xx` = the server), and the **Response** tab usually hands you the real cause.

Next: one real "why is this broken?" bug, walked end to end across all four panels.

```quiz
[
  {
    "q": "Why does a breakpoint generally beat scattering console.log statements?",
    "choices": [
      "It runs the code faster",
      "It pauses the code so you can inspect every local value and ask new questions, without refreshing between guesses",
      "It automatically fixes the bug it pauses on",
      "It works even when JavaScript is disabled"
    ],
    "answer": 1,
    "explain": "A breakpoint freezes execution and exposes all live state at once. A console.log only shows the one value you thought to print, and each new guess costs another edit and refresh."
  },
  {
    "q": "In the Network tab, a request shows status 500. What does that tell you?",
    "choices": [
      "Your request was malformed - fix the frontend",
      "The page was redirected somewhere else",
      "The server hit an error fulfilling the request - the problem is likely on the backend",
      "The resource was not found"
    ],
    "answer": 2,
    "explain": "5xx codes mean the server broke. 4xx codes mean your request was wrong. The 500 points you at the backend, and the Response tab often contains the actual server error message."
  },
  {
    "q": "The Network tab is empty when you open it after the page already loaded. Why?",
    "choices": [
      "The page made no requests at all",
      "Network only records while it's open - you need to reload with it open",
      "You need a paid plan to see requests",
      "The requests were all cached and are never shown"
    ],
    "answer": 1,
    "explain": "The Network tab records requests as they happen. Requests that fired before you opened it (or before a reload) aren't captured, so reload the page with the tab open."
  }
]
```


---

# A Real Investigation

A real bug doesn't announce which panel to open. What separates flailing from debugging is knowing the
*order* to reach for them. This phase walks one realistic bug end to end and introduces the last panel -
**Elements** - for when the page *looks* wrong rather than *behaves* wrong.

## The bug

A user reports: *"I click 'Add to cart' on the sale items and nothing happens. Works fine on regular items."*
One rule the whole way: **observe before you theorize** - open DevTools, look, let each panel point to the next.

## Step 1: Console first, always

You click "Add to cart" on a sale item. Nothing visibly happens. Before guessing, glance at the Console.

```console
❌ Uncaught TypeError: Cannot read properties of null (reading 'price')
       at addToCart (cart.js:54)
       at HTMLButtonElement.onclick (sale.js:31)
```
*What just happened:* The click *did* fire - `onclick` in sale.js called `addToCart`, which blew up on
`cart.js:54` reading `.price` off `null`. "Nothing happens" was never true; it threw and died silently -
the Console turned a vague report into a precise location.

## Step 2: Set a breakpoint where it broke

The error points at line 54. Open it in Sources, drop a breakpoint there, then click the sale button again
to pause at the crime scene.

```text
cart.js - PAUSED on :54

  52   function addToCart(productId) {
  53     const product = catalog.find(p => p.id === productId);
● 54     return { ...product, price: product.price };   ◄── paused, product = null
  55   }

  SCOPE
    productId = "sale-1099"
    product   = null              ◄── the find() returned nothing
```
*What just happened:* `product` is `null` - `catalog.find(...)` matched nothing for `"sale-1099"`. Either
the id is wrong, or the catalog never loaded this item.

Check the live prompt for the cheaper theory first:

```console
> catalog.length
40
> catalog.find(p => p.id === "sale-1099")
undefined
> catalog.filter(p => p.id.startsWith("sale")).length
0
```
*What just happened:* The catalog has 40 items but *zero* sale items - `addToCart` is correctly failing on
data that isn't there.

## Step 3: Network - did the sale data even arrive?

Sale items likely come from an API call. Open **Network**, filter to `Fetch/XHR`, and reload.

```text
Name                    Status   Type    Size    Time
──────────────────────────────────────────────────────
GET /api/catalog        200      fetch   8.0 kB   90 ms
GET /api/sale-items     403      fetch   180 B   60 ms   ◄── red
```
*What just happened:* `/api/sale-items` came back **403 Forbidden** while the catalog loaded fine (200) -
explaining the empty sale items, the `null` from `find`, the throw in `addToCart`. Click it to learn why:

```text
GET /api/sale-items  →  Response tab:
  { "error": "missing or expired session token" }
```
*What just happened:* No valid session token - the frontend isn't sending the auth the sale endpoint requires
(the public catalog endpoint doesn't need it, hence *it* worked). Root cause, in the server's own words.

## The Elements panel: when the page LOOKS wrong

That bug was *behavioral*. The other half of frontend bugs are *visual* - broken layout, wrong color,
misalignment. Reach for **Elements**: it shows the **live DOM** (the HTML right now, post-JavaScript) and
the **CSS the browser actually applied**.

Say the sale price should be red but renders gray. Right-click it → Inspect:

```text
Elements:
  <span class="price sale-price">$10.99</span>

Styles (winning rules at top, losing rules struck through):
  .sale-price { color: red; }            ◄── what you wrote
  .price      { color: gray; }           ◄── what actually won
```
*What just happened:* Both rules target the element, but `.price` won and painted it gray. Styles shows the
*real* cascade - every matching rule, which won, which got overridden: a specificity/order problem, `.price`
beats `.sale-price`. Test the fix instantly:

```text
> double-click color value, type "red", Enter
  → the price turns red on screen immediately
```
*What just happened:* Fix confirmed without touching a file or refreshing. Elements edits are a live
*experiment*, not a save - they vanish on reload - but prove what change will work before you write it in
the real CSS.

## The gotchas that send you chasing ghosts

Three traps waste more hours than any actual bug:

⚠️ **Stale cache - you're debugging old code.** You fix something, reload, bug's still there - the browser
served a *cached* copy of your old JS. Open DevTools → Network, tick **Disable cache** (only while DevTools
is open), reload. First thing to rule out if unsure you're looking at current code.

⚠️ **"undefined" in the live prompt - wrong scope.** You type a variable name and get `Uncaught
ReferenceError`. It's real but *local* to a function, and you're asking from global scope. To read a local,
you must be **paused on a breakpoint inside that function** - then the Console evaluates in that scope.

⚠️ **Minified line numbers that make no sense.** An error points at `app.js:1:48210`, gibberish - built/minified
code. Make sure **source maps** are loading (Phase 2) so DevTools shows your original source, or every
breakpoint and stack frame is in a language you didn't write.

## The method, distilled

A method you can reuse on any frontend bug:

```text
1. Console      → is there an error? where (file:line)?      → cart.js:54
2. Sources      → breakpoint there; what's actually true?    → product is null
3. Network      → did the data arrive? what status?          → 403 on /sale-items
4. Response     → why did it fail, in the server's words?    → "missing session token"
   (Elements    → for visual bugs: what CSS actually won?)
```
*What just happened:* Each panel answered one question and pointed at the next. You never guessed - you
*observed*, and the bug unwound from symptom to root cause. That ordered habit is the real takeaway.

## For builders

When you file or hand off a bug, attach what this loop found: the Console error, the failing request's
status and Response body, the exact `file:line`. "Add to cart fails because `/api/sale-items` returns 403 -
missing session token" gets fixed in minutes. "It doesn't work" takes days.

## Recap

1. **Order beats tool knowledge.** Start at the **Console**, follow the error to **Sources**, check
   **Network** when data's involved, read the **Response** for the server's own explanation.
2. **Observe before you theorize** - let each panel hand you the next question instead of guessing.
3. **Elements** is for *visual* bugs: it shows the live DOM and the CSS that actually won (overridden rules
   struck through), and lets you test fixes live.
4. Rule out the ghosts early: **stale cache** (Disable cache + reload), **wrong scope** (pause inside the
   function to read locals), and **missing source maps** (so line numbers mean something).

That's the toolkit. Four panels, one method - most "why is this broken?" mysteries don't stand a chance.

```quiz
[
  {
    "q": "A button 'does nothing' when clicked. Where do you look first, and why?",
    "choices": [
      "The Network tab, because it's always a server problem",
      "The Console, because 'nothing happens' is often code that threw and died silently",
      "The Elements panel, to check the button's CSS",
      "The Performance panel, to see if it was too slow"
    ],
    "answer": 1,
    "explain": "'Nothing happened' frequently means the click handler ran and threw an error. The Console shows that error and the exact file:line, turning a vague report into a precise starting point."
  },
  {
    "q": "You type a variable name in the Console and get a ReferenceError, but you know the variable exists. What's the likely cause?",
    "choices": [
      "The variable was deleted by the garbage collector",
      "The Console can only read numbers, not objects",
      "It's local to a function, and you're not paused on a breakpoint inside that function",
      "You need to enable the variable in settings"
    ],
    "answer": 2,
    "explain": "The Console evaluates in the current scope. A function-local variable is only visible when you're paused on a breakpoint inside that function - then the Console reads that paused scope."
  },
  {
    "q": "In the Elements panel's Styles pane, a CSS rule appears struck through. What does that mean?",
    "choices": [
      "The rule has a syntax error",
      "The rule matched the element but was overridden by a more specific or later rule",
      "The rule is disabled and will never apply",
      "The rule belongs to a different element"
    ],
    "answer": 1,
    "explain": "Struck-through rules matched the element but lost the cascade to a winning rule. The Styles pane shows the real cascade, so you can see exactly which rule painted the element and which got overridden."
  }
]
```
