completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Advanced debugging

Learn conditional breakpoints, logpoints, async stack traces, source maps, and Node.js debugging workflows for production JavaScript bugs.

By the end, you can
  • 01
    Stop the code only when it mattersPick the right breakpoint: one with a condition, one for a page change or an event, or a logpoint that prints without stopping.
  • 02
    Read stack traces for async codeRead a stack trace that goes through await, and keep the first error with error.cause, console.trace, and Error.captureStackTrace.
  • 03
    Debug code as it runs in real appsFind a bug step by step, cut a list of changes in half to find the bad one, follow source maps, and debug Node.js.

Advanced debugging means asking smaller questions

You already know the basics from Debugging in the browser. You can set a breakpoint: a mark on a line that makes the code stop there, so you can look around. You can step through code one line at a time, and you can read the values of variables.

You also learned in Reading error messages that the top line of a stack trace shows where an error was thrown. That is a clue, not always the cause. This lesson builds on those skills.

Definition

Advanced debugging is a way of finding a bug step by step. You make the bug happen on purpose, you shrink the code around it, and you use the right tool to test one guess at a time. You stop when one guess is proven.

The tools in this lesson do more than a plain breakpoint. Here is the list. Each one is explained when it comes up:

  • breakpoints that stop only when a condition is true;
  • logpoints, which print a message without stopping;
  • breakpoints that stop when the page changes, an event fires, or a request starts;
  • a setting that stops the code when an error is thrown;
  • the Ignore List, which skips code you didn't write;
  • stack traces for async code;
  • source maps, and the Node.js debugger.

The skill is to pick the smallest tool that answers your current question.

Real-life analogyChecking a shopping list

You come home from the shop, and something is missing. You take out your list. You check one item at a time, and you cross off each item that is in the bag. The item left over is the one you forgot.

In real life: Notice that something is missing
In JavaScript: Make the wrong result happen again
In real life: Check one item at a time
In JavaScript: Look at one part of the code at a time
In real life: Cross off what is there
In JavaScript: Drop each guess that turns out to be wrong

This lesson uses ideas from try/catch, custom errors, async/await, promise errors, bundlers and source maps, and JavaScript runtimes.

A method comes before the tools

WORKFLOW

A tool only helps when you can make the bug happen again and again. So start with a simple method, and repeat it until the bug is fixed:

  1. Make the bug happen.
  2. Shrink the code until only the failing part is left.
  3. Make one guess about the cause.
  4. Check that guess by looking at real values.
  5. Fix the smallest line, then run everything again.
A small program that shows the checkout bugPop out in the code editor (opens in a new tab)JavaScript
function totalAfterDiscount(price, discount) {
  return price - discount;
}
console.log(totalAfterDiscount(20, 10));

Line 4 calls the function with a price of 20 and a discount of 10 percent. Line 2 takes away 10 as if it were 10 dollars, not 10 percent. So line 4 prints 10. The right answer is 18.

This tiny program has no page, no buttons, and no network. It still shows the bug. So the bug must be in the formula on line 2. A small program like this is called a minimal reproduction: the least code that still makes the bug happen.

The fixed formula, with the same inputsPop out in the code editor (opens in a new tab)JavaScript
function totalAfterDiscount(price, discount) {
  return price * (1 - discount / 100);
}
console.log(totalAfterDiscount(20, 10));

On line 2, discount / 100 turns 10 percent into 0.1. Then 1 - 0.1 is 0.9, and 20 times 0.9 is 18. So line 4 prints 18. Keep the same inputs, 20 and 10, when you test the fix. With new inputs, you can't tell whether the fix worked.

  1. Make it happen: write down the exact steps, the input, the browser or Node version, and the test command.
  2. Shrink it: remove code, data, requests, and changes that don't matter, as long as the bug still happens.
  3. Guess: say one cause that a test could prove wrong, like "the third click sends the form twice".
  4. Check: pick the tool that disturbs the program least: a condition, a logpoint, a watched value, a stack trace, or a failing test.

Sometimes the code worked last week, and now it doesn't. Somewhere in a list of changes, one change broke it. You can find that change fast with bisecting, which means cutting the list in half again and again:

  • Test the app with the changes up to the middle of the list.
  • If the test fails, the bad change is in the first half. Keep that half.
  • If the test passes, the bad change is in the second half. Keep that half.
  • Repeat until one change is left.
Six changes in a row, oldest to newest: route, layout, copy, discount, theme, receipt. Test 1, through copy, passes. Test 2, through theme, fails. Test 3, through discount, fails. So discount is the first bad change.1. pass3. fail2. failroutelayoutcopydiscountthemereceiptoldestnewestfirst bad change
Each test runs the app with every change up to that card. Three tests are enough to find the first bad change, discount, out of six.

One more trick: explain the code out loud, line by line, to a friend or even to a toy duck. This is called rubber-ducking. When you say each value out loud, you often notice the one you never checked.

Find the bad change by cutting the list in half
A helper that cuts the list in halfPop out in the code editor (opens in a new tab)JavaScript
const changes = [  { id: "a-route", label: "add account route" },  { id: "b-layout", label: "move checkout layout" },  { id: "c-copy", label: "rewrite button copy" },  { id: "discount-formula", label: "replace discount formula" },  { id: "e-theme", label: "adjust theme tokens" },  { id: "receipt-cache", label: "cache receipt payload" },]; function testUpTo(index) {  return changes.slice(0, index + 1).some(change => change.id === "discount-formula")    ? "fail"    : "pass";} function bisectFirstBad() {  let low = 0;  let high = changes.length - 1;  const steps = [];  while (low < high) {    const mid = Math.floor((low + high) / 2);    const result = testUpTo(mid);    steps.push(`test through ${changes[mid].id}: ${result}`);    if (result === "fail") high = mid;    else low = mid + 1;  }  return { first: changes[low], steps };} const report = bisectFirstBad();console.log(report.first.label);console.log(report.steps.join(" | "));
What the search founddiscount-formula
first bad changereplace discount formula
tests runtest through c-copy: pass | test through e-theme: fail | test through discount-formula: fail
Try it yourself

Cutting the list in half found discount-formula after 3 tests, without testing every change one by one.

Pick which change holds the bug. In real projects, git bisect does the same search over saved versions of the code.

In a real project, git bisect does this search for you. Git is the tool that keeps the history of your code, and a commit is one saved change in that history. You tell git one commit that works and one that fails. Git then picks commits in between, and you run the same test on each one.

Use a test that gives the same result every time. If the test sometimes passes and sometimes fails, the search gives a wrong answer. When you are done, git bisect reset takes you back to the code you started with. Type these commands in a terminal, not in the editor.

Find the bad commit with git bisect, in a terminalbash
# In a clean working tree, replace GOOD_SHA with a commit known to pass.
git bisect start
git bisect bad HEAD
git bisect good GOOD_SHA
# Run the same checkout test at each checked-out commit.
git bisect good  # test passes; otherwise use git bisect bad
git bisect reset # return to the original branch when finished
Keep a debugging journal

Write down how to make the bug happen, what you tried, what you saw, and what you changed. Then you won't try the same guess again and again, like "maybe it's the server", for an hour.

Conditional breakpoints and logpoints

DEVTOOLS

A plain breakpoint stops every time its line runs. In a loop over 500 items, that is 500 stops. Two tools help:

  • A conditional breakpoint stops only when a condition you type is true.
  • A logpoint prints a message to the Console each time the line runs, and never stops.

In Chrome DevTools, open the Sources panel. Right-click a line number. Then choose Add conditional breakpoint... or Add logpoint.... Firefox uses the same names.

Find one item in the cartPop out in the code editor (opens in a new tab)JavaScript
const cart = [{ id: 17 }, { id: 42 }];
for (const item of cart) {
  if (item.id === 42) console.log(item.id);
}

The loop visits both items. The condition on line 3 is false for 17 and true for 42. So the program prints 42 once.

In DevTools you can ask the same question without editing the code. Put a conditional breakpoint on line 3 with the condition item.id === 42. The code stops only for that item. This snippet only acts out the condition; running it doesn't create a breakpoint.

Print every item in the cartPop out in the code editor (opens in a new tab)JavaScript
const cart = [{ id: 17 }, { id: 42 }];
for (const item of cart) {
  console.log("checking", item.id);
}

Here line 3 prints checking 17, then checking 42. A logpoint gives you the same two lines without editing the file. Its message would be "checking", item.id. The message is real JavaScript, so it runs each time. Don't put code in it that changes values, and don't print private data.

Left: a conditional breakpoint with the condition item.id === 42. For item 17 the condition is false, so the code keeps going. For item 42 it is true, so the code pauses. Right: a logpoint with the message checking, item.id. The console shows checking 17 and checking 42, and the code never pauses.item.id === 42conditional breakpointid: 17id: 42falsekeeps goingtruepauses here"checking", item.idlogpointconsolechecking 17checking 42prints, never pauses
A conditional breakpoint stops the code, but only when its condition is true. A logpoint never stops the code; it only prints a line each time.

DevTools has more kinds of breakpoints. The comments below list the names to look for in Chrome. They are not code you can run.

Breakpoint names to look for in Chrome DevToolsJavaScript
// Chrome DevTools Sources panel names to look for:// Add conditional breakpoint...    item.id === 42// Add logpoint...                  "item", item.id, item.stock// DOM breakpoints                  subtree modifications, attribute modifications, node removal// Event Listener Breakpoints       click, input, timer, animation, and more// XHR/fetch Breakpoints            pause when the request URL contains text// Pause on exceptions              uncaught only, or caught and uncaught// Ignore List                      skip framework or extension scripts while stepping

A condition runs where the line is, so it can use the variables there, like item. You can also count calls. Say a bug happens on the third click. Put a conditional breakpoint on the line that adds 1 to the count, with the condition clickCount === 2. The count is still 2 when the third click reaches that line, so the code stops on the third click.

A logpoint prints without stopping. That matters when stopping would change what you are looking at, like a timer or an animation.

Some bugs aren't about one line. These breakpoints stop the code when something happens on the page:

  • DOM breakpoint. The DOM is the page's tree of elements. If a pop-up box vanishes, right-click it in the Elements panel. Choose Break on, then Node removal. The code stops on the line that removes it. If the box is replaced instead, choose Subtree modifications on its parent.
  • Event listener breakpoint. If you don't know which function handles a click, open Sources, then Event Listener Breakpoints, and tick click. The code stops inside the function when a click happens.
  • XHR/fetch breakpoint. If the checkout sends a request to the wrong address, open Sources, then XHR/fetch Breakpoints, and add /checkout. The code stops when a request whose address contains that text starts. It looks only at the address, not at the answer from the server.
Conditional breakpoint: stop on the item you care about
Step 0 of 10Ready
Your turn: follow the blue line

Step through a loop the way a conditional breakpoint sees it: run past the items that don't match, then stop on the first item that does.

Running in
  1. script
Next: line 1
  { id: 17, label: "keyboard", stock: 4 },  { id: 42, label: "monitor", stock: -1 },  { id: 58, label: "dock", stock: 3 },]; function firstBrokenItem(items, breakWhen) {  const log = [];  for (const item of items) {    log.push(`checking ${item.id}`);    if (breakWhen(item)) {      return { item, log };    }  }  return { item: null, log };} const result = firstBrokenItem(items, item => item.id === 42);console.log(result.item.id);console.log(result.log.join(" -> "));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the condition

A new condition starts the replay again. Both conditions find the same item, 42, for different reasons.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Find the first matching item
A helper that finds the first matching itemPop out in the code editor (opens in a new tab)JavaScript
const items = [  { id: 17, label: "keyboard", stock: 4 },  { id: 42, label: "monitor", stock: -1 },  { id: 58, label: "dock", stock: 3 },]; function firstBrokenItem(items, breakWhen) {  const log = [];  for (const item of items) {    log.push(`checking ${item.id}`);    if (breakWhen(item)) {      return { item, log };    }  }  return { item: null, log };} const result = firstBrokenItem(items, item => item.id === 42);console.log(result.item.id);console.log(result.log.join(" -> "));
What the helper foundid
matching item42: monitor
items checkedchecking 17 -> checking 42
Try it yourself

The helper found the first item where item.id === 42 is true: item 42, the monitor.

This is not a real debugger. It is a small function that asks the same question as a conditional breakpoint: which is the first item where the condition is true?

A logpoint sits between console.log and a breakpoint. It gives you a clue, and the program keeps running. The tool below is not DevTools. It is a small function that acts like a logpoint: it prints only when a condition is true, and it doesn't change what the code does.

A function that acts like a logpoint
A function that prints only when a condition is truePop out in the code editor (opens in a new tab)JavaScript
function withConditionalTrace(fn, shouldLog) {  return (...args) => {    const result = fn(...args);    if (shouldLog(args, result)) {      console.log(`${fn.name}(${args.join(", ")}) -> ${result}`);    }    return result;  };} const priceWithTax = (price, rate) => Number((price * (1 + rate)).toFixed(2));const tracedPrice = withConditionalTrace(priceWithTax, (args, result) => result > 50);console.log(tracedPrice(20, 0.1));console.log(tracedPrice(60, 0.1));
Console outputslow-only
  1. 22
  2. priceWithTax(60, 0.1) -> 66
  3. 66
Try it yourself

Only the call whose result is greater than 50 prints the extra line, like a logpoint with a condition.

A real DevTools logpoint is better, because it doesn't change your code at all. This small function shows the same idea: print only when a condition is true.
Which breakpoint fits this bug?
  • A loop has 500 items, but only the item with id 42 goes wrong.
  • The bug happens on the third click, and not before.
  • You want to print total from a built file, without changing the code.
  • A pop-up box vanishes, and no one knows which code removed it.
  • Some function you can't find runs every time you type in a box.
  • A catch handles an error before you can see where it was thrown.
  • The checkout sends its request to the wrong address.
Try it yourself
0 of 7 correct

Put each bug under the first DevTools tool you would try. Other tools can help later. Pick the smallest first step.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

The table below compares four ways to look inside running code. Read each row as: "use this tool when..., and watch out for...".

Four ways to look inside running code
ToolWhen to use itWhat to watch out for
console.logYou can edit the code on your own computer, and you want it to print something each time you test.It changes your code. You can forget to remove it, and the built file may not have it.
LogpointYou want a line to print to the Console, without changing the file and without stopping.It only exists in your DevTools. It can't replace logging in the live app.
Conditional breakpointA line runs many times, but you care about one case, like item.id === 42.The condition is real code. If it changes a value, it changes your program.
debugger; statementYou want a quick stop while you test, or the right line is hard to click on.Remove it before you share code. It stops the code for anyone who has DevTools open.

Stack traces for async code

STACKS

A stack trace is the list of function calls that were running when an error happened. Each line is one call.

Async code makes this harder. When a function hits await, it pauses. Later it starts again, but by then the functions that called it have stopped running. Older engines lost those callers. Their stack traces ended at the await.

V8 is the JavaScript engine inside Chrome and Node. Modern V8 remembers which function was waiting with await for which. So its stack traces can show lines like at async outer under the function that threw.

Catch one failed cart loadPop out in the code editor (opens in a new tab)JavaScript
async function loadCart() {
  throw new Error("cart unavailable");
}
loadCart().catch(error => console.log(error.message));

Line 2 throws inside an async function, so the promise it returns is rejected. The .catch on line 4 prints cart unavailable. Without the .catch, the error would stay inside a rejected promise.

To see the stack, keep the error object itself. Don't replace it with a new message. The next example does that.

Print a stack trace that goes through awaitJavaScript
Error.stackTraceLimit = 20; async function leaf() {  await Promise.resolve();  throw new Error("boom from leaf");} async function middle() {  await leaf();} async function outer() {  await middle();} outer().catch(error => {  console.log(error.stack.split("\n").slice(0, 5).join("\n"));});

Line 1 asks V8 to keep up to 20 lines of stack. Lines 3 to 14 build a chain: outer waits for middle, and middle waits for leaf. Line 5 throws after an await. Line 17 prints the first five lines of the stack.

Three functions: leaf, middle, and outer. outer awaits middle, and middle awaits leaf. leaf throws. Node prints Error: boom from leaf, then at leaf, at async middle, and at async outer. Each function points at its own line.functionsleaf()middle()outer()NodeError: boom from leafat leafat async middleat async outer
outer waits for middle, and middle waits for leaf. When leaf throws, Node still lists both waiting functions. File names are left out here.

Stack traces are text, and each engine writes them its own way. V8 writes lines like at async outer (file.js:12:3). Firefox often writes outer@file.js:12:3. So read a stack trace as a clue for people, not as data your code depends on.

The DevTools Call Stack panel can show even more. It can link a callback to the code that scheduled it. The error.stack text only shows what the engine saved. Some timers and callbacks don't remember who created them. When that link is missing, keep the original error as a cause, or print a stack where the work is scheduled.

Hide a helper function from the stack with Error.captureStackTraceJavaScript
function createInputError(message) {  const error = new Error(message);  Error.captureStackTrace?.(error, createInputError);  return error;} function validateCart() {  return createInputError("missing total");} console.log(validateCart().stack);

Error.captureStackTrace only exists in V8. It rebuilds an error's stack and leaves out the function you pass, plus everything above it. On line 3, that is createInputError. So the stack starts at validateCart, the code that asked for the error.

The ?. on line 3 is optional chaining: if captureStackTrace doesn't exist, as in Firefox, the call is skipped. The error still works without it.

Keep the original error with causePop out in the code editor (opens in a new tab)JavaScript
const networkError = new Error("request failed");const checkoutError = new Error("checkout failed", { cause: networkError }); console.log(checkoutError.message);console.log(checkoutError.cause.message);

Line 2 makes a new error and passes the first one as cause. Line 4 prints checkout failed. Line 5 prints request failed. Both messages stay, and neither is mixed into the other.

Two more tools help here:

  • console.trace() prints the current stack without throwing an error.
  • Error.stackTraceLimit sets how many lines of stack V8 keeps.

Use them while you debug. Don't build your program's logic on them.

console.trace prints a stack without throwingJavaScript
function leaf() {  console.trace("discount path");} function outer() {  leaf();} outer();

Debugging with source maps

MAPPING

The code a browser runs is often not the code you wrote. Build tools change it first:

  • they turn TypeScript into JavaScript;
  • they join many files into one;
  • they minify it, which means they remove spaces and shorten names to make the file smaller.

The new file is called generated code. A source map is a separate file that links each line of generated code back to the line you wrote. With it, DevTools can show your own file and let you put breakpoints in it.

An error keeps its message without a mapPop out in the code editor (opens in a new tab)JavaScript
function checkout(total) {
  throw new Error("bad total: " + total);
}
try { checkout(0); } catch (error) { console.log(error.message); }

Line 2 throws, and the catch on line 4 prints bad total: 0. A source map would not change this message. It also would not change how the code runs. It only changes which file and line a stack trace points to.

If DevTools still shows the generated file, check two things. Does the map file load? Was it made by the same build as the code?

The TypeScript you wroteTypeScript
export function originalName(total: number) {  throw new Error("mapped failure for " + total);} originalName(42);
The JavaScript that runs, with a link to its mapJavaScript
export function originalName(total) {  throw new Error("mapped failure for " + total);}originalName(42);//# sourceMappingURL=checkout.js.map

Line 5 of the generated file is a comment: //# sourceMappingURL=checkout.js.map. It tells DevTools or Node where to find the map. The throw is on line 2 in both files, so the map leads from line 2 of the JavaScript to line 2 of the TypeScript.

Line 2 of checkout.js, the file that runs, points to the map file checkout.js.map. The map points to line 2 of checkout.ts, the file you wrote.checkout.js:2what runscheckout.js.mapthe mapcheckout.ts:2what you wrote
The browser or Node runs checkout.js. The map file says that its line 2 came from line 2 of checkout.ts, so the debugger can show you the file you wrote.

In Chrome you can turn source maps on or off. Open Settings, then Preferences, then Sources, and find Enable JavaScript source maps.

Chrome also has an Ignore List. Files on it, like library or framework code, are skipped when you step through code. Older tools called this blackboxing, and Firefox still uses that word.

The table below shows where source maps are used. Read each row as: "in this place, the map does this, and here is a good habit".

Where source maps are used
WhereWhat the map doesA good habit
Browser DevToolsIt shows the code you wrote in place of the shortened or changed code that runs.Turn it on in Settings, then Preferences, then Sources: Enable JavaScript source maps.
The live websiteTeams often keep maps private and send them only to their error-tracking service.Build hidden maps, and don't put a sourceMappingURL comment in public files.
Node.jsIt makes stack traces point to your TypeScript or original files, not the generated JavaScript.Run node --enable-source-maps dist/file.js, and keep the .map file next to the generated file.
Source maps in the terminal and in productionbash
# Browser builds usually attach a comment near the end of the file:# //# sourceMappingURL=checkout.js.map # Node can remap stack traces when the .js file and .map file are present:node --enable-source-maps dist/checkout.js # Many production web builds keep maps private instead:# use a hidden-source-map setting, upload the .map files to your error tracker,# and do not publish sourceMappingURL comments for users to download.
Keep private code private

Anyone who can download a source map can read your original code and file names. For open-source code, that may be fine. For a private app, it may not be. Many teams send their maps only to their error-tracking service, and don't put them on the public site.

Debugging Node.js

RUNTIME

Node has a built-in debugger connection called the inspector. Chrome DevTools can connect to it. Here is how:

  1. Start your program with node --inspect.
  2. In Chrome, open chrome://inspect.
  3. Click your program to connect DevTools to it.

If the bug happens while the program is starting, use --inspect-brk instead. It stops before the first line of your code runs, so you have time to connect.

Commands that start Node with a debuggerbash
# Start Node with the inspector and attach Chrome DevTools from chrome://inspect.node --inspect server.jsnode --inspect-brk server.js # Debug the built-in test runner before the first line runs.node --test --inspect-brk tests/checkout.test.mjs # Use the terminal debugger when you only have a shell.node inspect server.js

There are other ways to debug Node:

  • VS Code's JavaScript Debug Terminal starts every Node program in it with the debugger on.
  • A launch.json file saves your debug settings, so the whole team starts the program the same way.
  • node inspect is a debugger that works right in the terminal.
  • node --test --inspect-brk stops Node's built-in test runner before the test file runs.
Node tools for printing objects and catching last errorsJavaScript
const util = require("node:util");console.log(util.inspect({ cart: { total: 18 } }, { depth: 2 })); process.on('uncaughtException', (error) => {  console.error(error);  process.exitCode = 1;}); process.on('unhandledRejection', (reason) => {  console.error(reason);  process.exitCode = 1;});

Lines 1 and 2 print an object with util.inspect(value, { depth: 2 }). The depth sets how many levels of nested objects to show.

Lines 4 to 12 listen for two events. uncaughtException fires when an error is never caught. unhandledRejection fires when a rejected promise has no .catch. These handlers are a last chance to report the problem. Log the error, set the exit code to 1 so the program counts as failed, and then fix the code that threw. Don't act as if everything is fine.

You can also run a program with NODE_DEBUG=http,net in front of the command. Node then prints its own notes about those built-in parts, here web requests and network connections.

Never open the inspector to the internet. Anyone who connects to it can run any code inside your program. Keep it on your own computer, and use a protected connection to debug a remote server.

Real bugs: the third click, a wrong value, and a hidden error

REAL BUGS

Here are three bugs you will meet in real apps, and the tool that fits each one first:

  • A bug that happens only on the third click: a conditional breakpoint on the line that adds 1 to the count.
  • A click handler you can't find: an event listener breakpoint.
  • A wrong value in a loop: a condition like item.id === 42, or a logpoint.
  • An error that a catch hides: stop on caught errors, read the async stack, and keep the cause.
Third-click bug: stop with a condition
Step 0 of 15Ready
Your turn: follow the blue line

This replay shows a bug that happens only on the third click. Step until the condition is true, then look at the flag that changed.

Running in
  1. script
Next: line 1
let submitted = false; function handleCheckoutClick() {  clickCount += 1;  if (clickCount === 3) {    submitted = true;    return "submitted twice";  }  return "waiting";} console.log(handleCheckoutClick());console.log(handleCheckoutClick());console.log(handleCheckoutClick());
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The Console has three more tools for quick checks:

  • console.table shows a list of objects as a table with rows and columns.
  • console.time starts a timer, and console.timeEnd stops it and prints how long it took.
  • console.assert prints a message when something you expected is false. It doesn't stop the program.
Show the cart as a tablePop out in the code editor (opens in a new tab)JavaScript
const cart = [{ item: "tea", stock: 0 }, { item: "milk", stock: 2 }];
console.table(cart);
console.log(cart.find(row => row.stock === 0).item);

Line 1 makes two rows. Line 2 shows them as a table, with an item column and a stock column. Line 3 finds the row with no stock and prints tea. Don't print customers' private details to a Console that other people can see.

Time one cart checkPop out in the code editor (opens in a new tab)JavaScript
console.time("cart check");
const cart = ["tea", "milk"];
console.log(cart.includes("tea"));
console.timeEnd("cart check");

Lines 1 and 4 use the same label, "cart check", so they belong to the same timer. Line 3 prints true. The time printed by line 4 is different on every run, so don't treat it as an exact speed test. Give each timer its own label when two run at the same time.

Report a stock check that failedPop out in the code editor (opens in a new tab)JavaScript
const cart = { stock: 0 };
console.assert(cart.stock > 0, "tea is out of stock");
console.log("checkout continues");

Stock is 0, so cart.stock > 0 is false. Line 2 prints Assertion failed: tea is out of stock. Then line 3 still runs and prints checkout continues. When a failure must stop the program, throw an error or use a test instead.

Now the hidden error. A catch block that hides an error is said to swallow it. The code below swallows one.

A catch block hides the real failurePop out in the code editor (opens in a new tab)JavaScript
async function saveOrder() {  throw new Error("network down");} async function checkout() {  try {    await saveOrder();    return "saved";  } catch (error) {    return "saved"; // bug: the rejection was swallowed  }} checkout().then(status => console.log(status));

Line 2 throws network down. Line 9 catches it. Then line 10 returns "saved" anyway. So line 14 prints saved, even though nothing was saved.

To find a bug like this, turn on Pause on caught exceptions in DevTools. The code then stops on line 2, where the error is thrown, even though a catch handles it later. Then fix the catch so callers learn about the failure and the original error stays attached.

Pass the failure on and keep the original errorPop out in the code editor (opens in a new tab)JavaScript
async function saveOrder() {  throw new Error("network down");} async function checkout() {  try {    await saveOrder();    return { ok: true };  } catch (error) {    throw new Error("checkout failed", { cause: error });  }} checkout().catch(error => console.log(error.cause.message));

Now line 10 throws a new error with the original as its cause. The .catch on line 14 reads error.cause.message and prints network down.

A debugger statement for a quick pauseJavaScript
function submitForm(form) {  // Temporary local-only pause. Remove it before sharing code.  debugger;  return form.checkValidity();}

The debugger; statement on line 3 works like a breakpoint written in the code. It stops the code only when DevTools or another debugger is open. Otherwise the function runs as usual. It helps when you can't easily click on the right line, as in generated files. Remove it before you share your code.

Common misconceptions

"A conditional breakpoint is safe because it only reads values."

The condition is real JavaScript, and it runs. If it calls a function that changes something, it changes your program.

"Logpoints can replace logging in the live app."

Logpoints exist only in your own DevTools, and only while you debug. Logging in a live app must be written in the code, checked for private data, and stored on purpose.

"Async stack traces look the same everywhere."

Each engine writes them its own way. In tests, check for important function names, not exact file paths or line numbers.

"Source maps fix the bug."

They only show you which line of your own code ran. You still have to make the bug happen, shrink it, guess, and check.

"An unhandledRejection handler means the app recovered."

It only means you reported the failure at the last moment. Treat the program as broken until you fix the code that failed.

The table below lists three pairs of ideas that are easy to mix up. Read each row as: "these two get mixed up, here is the difference, and here is what to do".

Debugging ideas that are easy to mix up
What gets mixed upWhat is really trueWhat to do
Stopping the code, or only watching itStopping changes the timing. Watching might be enough.Try a logpoint before you stop an animation, a timer, or two requests racing each other.
The line that threw, or the real causeThe top line of the stack threw. A caller may have passed it bad data.Read down the stack, and keep the original error in error.cause.
The file that runs, or the file you wroteThe browser runs generated JavaScript.Use source maps to see and debug the file you wrote.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upWrite the condition

A loop goes wrong only for the item whose id is 42. What condition should the conditional breakpoint use?

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePredict the third click

    Read the starter code. What text does the third click return?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    let clickCount = 0;
    let submitted = false;
    
    function handleCheckoutClick() {
      clickCount += 1;
      if (clickCount === 3) {
        submitted = true;
        return "submitted twice";
      }
      return "waiting";
    }
    
    console.log(handleCheckoutClick());
    console.log(handleCheckoutClick());
    console.log(handleCheckoutClick());

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRead the logpoint-style output

      Which extra line prints only for the call with the bigger result?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function withConditionalTrace(fn, shouldLog) {
        return (...args) => {
          const result = fn(...args);
          if (shouldLog(args, result)) {
            console.log(`${fn.name}(${args.join(", ")}) -> ${result}`);
          }
          return result;
        };
      }
      
      const priceWithTax = (price, rate) => Number((price * (1 + rate)).toFixed(2));
      const tracedPrice = withConditionalTrace(priceWithTax, (args, result) => result > 50);
      console.log(tracedPrice(20, 0.1));
      console.log(tracedPrice(60, 0.1));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeName the first bad change

        Follow the search in your head. Which change label does it print first?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const changes = [
          { id: "a-route", label: "add account route" },
          { id: "b-layout", label: "move checkout layout" },
          { id: "c-copy", label: "rewrite button copy" },
          { id: "discount-formula", label: "replace discount formula" },
          { id: "e-theme", label: "adjust theme tokens" },
          { id: "receipt-cache", label: "cache receipt payload" },
        ];
        
        function testUpTo(index) {
          return changes.slice(0, index + 1).some(change => change.id === "discount-formula")
            ? "fail"
            : "pass";
        }
        
        function bisectFirstBad() {
          let low = 0;
          let high = changes.length - 1;
          const steps = [];
          while (low < high) {
            const mid = Math.floor((low + high) / 2);
            const result = testUpTo(mid);
            steps.push(`test through ${changes[mid].id}: ${result}`);
            if (result === "fail") high = mid;
            else low = mid + 1;
          }
          return { first: changes[low], steps };
        }
        
        const report = bisectFirstBad();
        console.log(report.first.label);
        console.log(report.steps.join(" | "));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeKeep the original error

          Which property links the new error to the original error?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          async function saveOrder() {
            throw new Error("network down");
          }
          
          async function checkout() {
            try {
              await saveOrder();
              return { ok: true };
            } catch (error) {
              throw new Error("checkout failed", { cause: error });
            }
          }
          
          checkout().catch(error => console.log(error.cause.message));

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            8 QUESTIONS

            Some questions ask which tool to use. Others ask what code prints. For code, follow the values line by line.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8A loop goes wrong only for the item whose id is 42. Which DevTools tool fits best?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this loop search print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const items = [{ id: 17 }, { id: 42 }];
              const hit = items.find(item => item.id === 42);
              console.log(hit.id);

              Choose an answer to see the explanation.

            3. Question 3 of 8What can a logpoint do that a console.log in your code can't?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this code with a cause print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const networkError = new Error("request failed");
              const checkoutError = new Error("checkout failed", { cause: networkError });
              console.log(checkoutError.cause.message);

              Choose an answer to see the explanation.

            5. Question 5 of 8Why can a stack trace show at async outer even after an await?

              Choose an answer to see the explanation.

            6. Question 6 of 8Your source maps show private code. What should your live website avoid?

              Choose an answer to see the explanation.

            7. Question 7 of 8Which command stops a Node program before its first line, so you can connect a debugger?

              Choose an answer to see the explanation.

            8. Question 8 of 8What does the third click print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let clickCount = 0;
              function click() {
                clickCount += 1;
                return clickCount === 3 ? "submitted twice" : "waiting";
              }
              click();
              click();
              console.log(click());

              Choose an answer to see the explanation.

            Key takeaways

            • Start with the method: make the bug happen, shrink it, guess one cause, and check it. Tools come after.
            • Each kind of breakpoint answers a different question: a condition, a logpoint, a page change, an event, a request, or a thrown error.
            • Modern stack traces can follow await chains, but each engine writes them its own way.
            • Source maps lead from the code that runs back to the code you wrote. Keep them private when your code is private.
            • For Node, use --inspect, VS Code, node inspect, or NODE_DEBUG, and treat last-chance error handlers as reports, not fixes.

            Remember the one-liner.
            Advanced debugging is not more guessing. It is collecting small, clear clues with the tool that disturbs your program the least.

            Up next: Creational patterns.

            CompleteFrontend Clear concepts. Working examples.