---
title: 'Source Debugging'
source: 'https://academia.sh/en/courses/browser-platform/source-debugging'
course: 'The Browser and the Web Platform'
language: en
updated: '2026-08-17T18:09:11+00:00'
license: 'CC BY-SA 4.0'
---

# Source Debugging

Stopping a program at a chosen point and reading its state; the question each breakpoint type answers, stepping and reading scope, resolving a location in production output back to source with a source map, and deploying the map.

The tools so far measured a result: which rule won, how long the request took, which
function held the main thread, which object stayed in memory. None answer "what was the
variable's value when this line ran."

Why a row of the measurement list rendered missing, which branch a recording took under
which condition — this is only seen by stopping the program at a chosen point and reading
its state there. This lesson covers the pause mechanism and how paused code in production
output is traced back to its source.

## What Pausing Means

When a breakpoint is reached, the main thread stops. What stops is not only that
function: it is the entire page. Timers do not fire, events are not processed, no frame
is produced. The state read is exactly the state at the moment that instruction was about
to run.

Two consequences follow. First, **time-dependent behavior breaks under pausing**: stopped
mid-animation and resumed, the elapsed time has actually passed, and duration-based
calculations jump. Timing problems are examined with a recording, not by pausing.

Second, **the value read at a stopped point is more reliable than a printed one.** An
object written to the log is read at the moment it is expanded in the console; that is
after the write, and the object may have changed in between. A breakpoint removes this
uncertainty.

## Breakpoint Types and the Question They Answer

The breakpoint type is chosen according to the question being asked.

**Line breakpoint** answers "was this line reached, and what was the state then." It is
the most basic type and is used when the line's code is already known.

**Conditional breakpoint** stops on the same line only when a condition holds. It is
needed to stop only on the one row that renders missing in a hundred-row list; an
unconditional breakpoint here stops a hundred times and becomes unusable.

**Logpoint** does not stop, it only writes an expression's value to the log. It is a way
to add a log line without changing the code, and so eliminates the temporary print lines
that would otherwise leak into version control.

**Exception breakpoint** answers "where was this error thrown from" and has two modes:
stop on every exception, or only on uncaught ones. The first mode finds places where an
error is caught and swallowed.

**Subtree modification breakpoint** answers "who changed this node." It stops on attribute
change, on child addition or removal, or on removal of the node itself. It is the
shortest route to finding who is responsible for an element that unexpectedly disappears
or changes; it requires no idea of which part of the code to search.

**Event breakpoint** stops when a given event type begins being handled and answers "which
listener is handling this click." In the structure established in the Event Delegation
lesson, a listener sitting on an ancestor far from the target makes this tool necessary.

**Request breakpoint** stops when a network request is made whose address matches a given
pattern, and shows the call chain that started the request.

## Stepping, Scope, and Call Stack

In a paused program, three views are read together.

The **call stack** says how execution got to that point. Every frame can be selected, and
the selected frame's scope is read; where a bad value came from is found by walking up the
stack. Asynchronous boundaries are added to the stack too: the relationship between code
running after a promise and the call that started the promise is shown as a separate
section. Without this, the stack starts at the event loop and says nothing.

The **scope view** lists local variables in the selected frame, values captured in a
closure, and the global scope separately. The closure section is especially valuable in
diagnosis: a variable being different from what is expected mostly comes from how a loop
variable was captured.

**Stepping** is done in three modes. Step over runs the call on the current line and stops
on the next line. Step into stops at the first line of the called function. Step out runs
until the current function returns and goes back to the caller.

Stepping's practical problem is falling into helper code: trying to step into an event
listener, intervening infrastructure frames scatter the stepping. Tools allow specific
files to be **ignored** for stepping; ignored files' frames are hidden in the stack, and
step-into skips them. Staying in your own code is achieved with this setting.

## Stopping in Production Output: Source Maps

The deployed script is not the written script: names are shortened, lines merged, perhaps
reduced to a different syntax. A program paused in this output shows a single unreadable
line.

The structure defined in the Source Maps lesson of the Tooling, Modules, and Ecosystem
course closes this gap: a mapping table tying every position in the generated file to a
position in the original file. The table is encoded with diffs and variable-length
quantities to save space. Decoding it is a computable operation.

```js
// sourcemap.mjs — VLQ encoding/decoding and resolving a generated position to its source
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

function vlqEncode(number) {
  let value = number < 0 ? (-number << 1) | 1 : number << 1; // sign in the lowest bit
  let output = "";
  do {
    let chunk = value & 0b11111;
    value >>>= 5;
    if (value > 0) chunk |= 0b100000;                  // continuation bit
    output += ALPHABET[chunk];
  } while (value > 0);
  return output;
}

function vlqDecode(text) {
  const numbers = [];
  let value = 0, shift = 0;
  for (const char of text) {
    const chunk = ALPHABET.indexOf(char);
    value += (chunk & 0b11111) << shift;
    if (chunk & 0b100000) { shift += 5; continue; } // continuation bit: move to the next char
    numbers.push(value & 1 ? -(value >>> 1) : value >>> 1);
    value = 0; shift = 0;
  }
  return numbers;
}

// Absolute mappings: position in the generated file → position in the source
const MAPPINGS = [
  [ // generated line 0
    { generatedColumn: 0, source: 0, line: 0, column: 0, name: 0 },
    { generatedColumn: 12, source: 0, line: 4, column: 2, name: 1 },
    { generatedColumn: 31, source: 0, line: 5, column: 14, name: null },
  ],
  [ // generated line 1
    { generatedColumn: 0, source: 0, line: 9, column: 0, name: null },
    { generatedColumn: 8, source: 0, line: 11, column: 4, name: null },
  ],
];

// Turns absolute mappings into a difference sequence and encodes it: fields are stored as diffs.
function generateMappings(mappings) {
  let prevSource = 0, prevLine = 0, prevColumn = 0, prevName = 0;
  return mappings.map((lineMappings) => {
    let prevGeneratedColumn = 0;
    return lineMappings.map((e) => {
      const fields = [e.generatedColumn - prevGeneratedColumn, e.source - prevSource,
        e.line - prevLine, e.column - prevColumn];
      if (e.name !== null) fields.push(e.name - prevName);
      prevGeneratedColumn = e.generatedColumn;
      prevSource = e.source; prevLine = e.line; prevColumn = e.column;
      if (e.name !== null) prevName = e.name;
      return fields.map(vlqEncode).join("");
    }).join(",");
  }).join(";");
}

const map = {
  version: 3,
  file: "measurements.min.js",
  sources: ["measurements.js"],
  names: ["drawMeasurements", "buildRow"],
  mappings: generateMappings(MAPPINGS),
};
console.log("mappings:", map.mappings);

// Decoding: reconstructs absolute positions from the difference sequence.
function parseMappings(mappings) {
  let source = 0, line = 0, column = 0, name = 0;
  return mappings.split(";").map((lineText) => {
    let generatedColumn = 0;
    if (lineText === "") return [];
    return lineText.split(",").map((segment) => {
      const a = vlqDecode(segment);
      generatedColumn += a[0]; source += a[1]; line += a[2]; column += a[3];
      if (a.length === 5) name += a[4];
      return { generatedColumn, source, line, column, name: a.length === 5 ? name : null };
    });
  });
}

const parsed = parseMappings(map.mappings);

// From generated position to source: take the LAST mapping in the line not past the column.
function findInSource(generatedLine, generatedColumn) {
  const lineMappings = parsed[generatedLine] ?? [];
  let found = null;
  for (const e of lineMappings) if (e.generatedColumn <= generatedColumn) found = e;
  if (!found) return null;
  return {
    file: map.sources[found.source],
    line: found.line + 1,
    column: found.column + 1,
    name: found.name === null ? null : map.names[found.name],
  };
}

console.log("\ngenerated position → position in source");
for (const [s, k] of [[0, 0], [0, 20], [0, 40], [1, 3], [1, 12]]) {
  const c = findInSource(s, k);
  console.log(`  measurements.min.js ${s + 1}:${k + 1}`.padEnd(28) +
    `→ ${c.file} ${c.line}:${c.column}` + (c.name ? `  (${c.name})` : ""));
}
```

```
mappings: AAAAA,YAIEC,mBACY;AAId,QAEI

generated position → position in source
  measurements.min.js 1:1   → measurements.js 1:1  (drawMeasurements)
  measurements.min.js 1:21  → measurements.js 5:3  (buildRow)
  measurements.min.js 1:41  → measurements.js 6:15
  measurements.min.js 2:4   → measurements.js 10:1
  measurements.min.js 2:13  → measurements.js 12:5
```

Three details in the output are worth noticing.

First, **the mapping is not complete**. There is no entry for the twenty-first column;
resolution falls back to the last mapping not past that column. This is why a breakpoint
in the debugger drifts a little from the line it was written on: it holds onto the
nearest mapping.

Second, **the name field is optional**. The third mapping has none; a shortened
variable's original name can only be recovered if its mapping carries a name. This is
why single-letter names keep showing up in the scope view.

Third, **the encoding is diff-based**. Decoding a segment depends on the ones before it;
reading cannot start from the middle of the table. This keeps the file small and forces a
full decode.

## Deploying the Map

A source map is a separate file, tied to the generated file with a comment line at its
end. The deployment decision comes down to three options.

**Serving the map publicly** is easiest and makes the source readable. For a closed-source
application, this means publishing the code.

**Generating the map but not publishing it** is the common middle path: the map is
uploaded to an error-tracking system, not served from the server. The user's browser keeps
the shortened stack trace; the developer side sees it resolved.

**Never generating it** produces no measurable gain — the map is only downloaded when
requested — and makes diagnosing production errors impossible.

Whether the map carries the source text inside itself is a separate decision too: if it
does, the file grows but the original files need no separate access; if not, the map
stays small and the sources need to be served separately.

## Summary

- A breakpoint stops the main thread; time-dependent behavior breaks under pausing, so
  timing problems are examined with a recording instead.
- Breakpoint type is chosen by the question asked: line, condition, log, exception,
  subtree change, event, and request.
- The call stack says how execution reached that point, the scope view gives values in
  the selected frame; asynchronous boundaries are added to the stack separately.
- Stepping has three modes; ignoring helper files keeps stepping in your own code.
- A source map is a diff-encoded mapping table; resolution falls to the nearest mapping,
  and the name field is optional.
- Map deployment is a decision between code readability and the diagnosability of
  production errors.

## Course Wrap-Up

This course treated the browser as a working environment and built it in four layers.

The **Document and Events** layer turned the page into a data structure that can be read
and modified from inside a program; the event's defined path from root to target and back
made it possible to manage many elements with a single listener, intervene in the
browser's default behavior, and build keyboard and form interaction.

The **Browser APIs** layer added persistence, navigation control, observation ability,
frame-synced updates, localized formatting, and the ability to offload the main thread to
the page.

The **Web Components and Offline** layer established two independences: an interface
piece's independence from the page — with its own name, lifecycle, encapsulated
structure, and style — and the application's independence from the network — with an
intervening worker, per-resource cache decisions, and an installable shell.

The **Developer Tools** layer put diagnosis on top of these three. Every tool's question
was separate: which rule won, where and how long a request was served from, where the
main thread got stuck, which object was never released, what the value was at that line.
The common method was the same each time — first determine the question, then read the
measure that answers it.

Everything this course built was done **by hand**: creating a node, binding a listener,
writing a change to the relevant element, undoing all of it when tearing down. The
measurement badge became a component, but the fifteen badges on the dashboard staying in
sync with the list still depends on hand-written update code. The code itself computes
which nodes should change when a record is added to the list; as this computation grows,
it turns into a source of bugs.

The next course, **M14/K05 · Component-Based Interface Development**, reverses this
computation. The interface is defined not by how to change it step by step, but by what
it **should be** given the current state; finding the difference and applying it to the
tree becomes the infrastructure's job. With platform capabilities and diagnostic tools now
in place, the turn is to the declarative interface model and the composition of
components.
