Advanced debugging
Learn conditional breakpoints, logpoints, async stack traces, source maps, and Node.js debugging workflows for production JavaScript bugs.
- 01Stop 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.
- 02Read stack traces for async codeRead a stack trace that goes through
await, and keep the first error witherror.cause,console.trace, andError.captureStackTrace. - 03Debug 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.
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
asynccode; - source maps, and the Node.js debugger.
The skill is to pick the smallest tool that answers your current question.
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
WORKFLOWA 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:
- Make the bug happen.
- Shrink the code until only the failing part is left.
- Make one guess about the cause.
- Check that guess by looking at real values.
- Fix the smallest line, then run everything again.
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.
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.
- Make it happen: write down the exact steps, the input, the browser or Node version, and the test command.
- Shrink it: remove code, data, requests, and changes that don't matter, as long as the bug still happens.
- Guess: say one cause that a test could prove wrong, like "the third click sends the form twice".
- 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.
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.
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(" | "));replace discount formulatest through c-copy: pass | test through e-theme: fail | test through discount-formula: failCutting the list in half found discount-formula after 3 tests, without testing every change one by one.
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.
# 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 finishedWrite 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
DEVTOOLSA 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.
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.
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.
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.
// 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 steppingA 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.
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.
script
{ 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(" -> "));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(" -> "));42: monitorchecking 17 -> checking 42The helper found the first item where item.id === 42 is true: item 42, the monitor.
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.
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));22priceWithTax(60, 0.1) -> 6666
Only the call whose result is greater than 50 prints the extra line, like a logpoint with a condition.
- A loop has 500 items, but only the item with
id42 goes wrong. - The bug happens on the third click, and not before.
- You want to print
totalfrom 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
catchhandles an error before you can see where it was thrown. - The checkout sends its request to the wrong address.
Put each bug under the first DevTools tool you would try. Other tools can help later. Pick the smallest first step.
The table below compares four ways to look inside running code. Read each row as: "use this tool when..., and watch out for...".
| Tool | When to use it | What to watch out for |
|---|---|---|
console.log | You 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. |
| Logpoint | You 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 breakpoint | A 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; statement | You 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
STACKSA 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.
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.
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.
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.
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.
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.stackTraceLimitsets how many lines of stack V8 keeps.
Use them while you debug. Don't build your program's logic on them.
function leaf() { console.trace("discount path");} function outer() { leaf();} outer();Debugging with source maps
MAPPINGThe 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.
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?
export function originalName(total: number) { throw new Error("mapped failure for " + total);} originalName(42);export function originalName(total) { throw new Error("mapped failure for " + total);}originalName(42);//# sourceMappingURL=checkout.js.mapLine 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.
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 | What the map does | A good habit |
|---|---|---|
| Browser DevTools | It 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 website | Teams 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.js | It 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. |
# 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.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
RUNTIMENode has a built-in debugger connection called the inspector. Chrome DevTools can connect to it. Here is how:
- Start your program with
node --inspect. - In Chrome, open
chrome://inspect. - 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.
# 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.jsThere are other ways to debug Node:
- VS Code's JavaScript Debug Terminal starts every Node program in it with the debugger on.
- A
launch.jsonfile saves your debug settings, so the whole team starts the program the same way. node inspectis a debugger that works right in the terminal.node --test --inspect-brkstops Node's built-in test runner before the test file runs.
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 BUGSHere 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
catchhides: stop on caught errors, read the async stack, and keep the cause.
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.
script
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());The Console has three more tools for quick checks:
console.tableshows a list of objects as a table with rows and columns.console.timestarts a timer, andconsole.timeEndstops it and prints how long it took.console.assertprints a message when something you expected is false. It doesn't stop the program.
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.
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.
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.
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.
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.
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".
| What gets mixed up | What is really true | What to do |
|---|---|---|
| Stopping the code, or only watching it | Stopping 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 cause | The 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 wrote | The browser runs generated JavaScript. | Use source maps to see and debug the file you wrote. |
Practice exercises
5 EXERCISESA loop goes wrong only for the item whose id is 42. What condition should the conditional breakpoint use?
const item = { id: 42 };
console.log(item.id === 42);item.id === 42 is true only for the item whose id is 42. So DevTools runs past every other item without stopping.
Read the starter code. What text does the third click return?
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());The third call returns submitted twice. To stop on that call, put a conditional breakpoint on the line that adds 1, with the condition clickCount === 2.
Which extra line prints only for the call with the bigger result?
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));The extra line is priceWithTax(60, 0.1) -> 66. It prints just before the normal 66.
Follow the search in your head. Which change label does it print first?
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(" | "));The first bad change is replace discount formula. In a real project, git bisect does this search over commits for you.
Which property links the new error to the original error?
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));throw new Error("checkout failed", { cause: error });The cause property keeps the first error. So your logs can show both checkout failed and the original network down.
Quiz: check your understanding
8 QUESTIONSSome questions ask which tool to use. Others ask what code prints. For code, follow the values line by line.
Question 1 of 8A loop goes wrong only for the item whose
idis 42. Which DevTools tool fits best?Choose an answer to see the explanation.
Question 2 of 8What does this loop search print?
Read the code, then predictconst items = [{ id: 17 }, { id: 42 }]; const hit = items.find(item => item.id === 42); console.log(hit.id);Choose an answer to see the explanation.
Question 3 of 8What can a logpoint do that a
console.login your code can't?Choose an answer to see the explanation.
Question 4 of 8What does this code with a cause print?
Read the code, then predictconst 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.
Question 5 of 8Why can a stack trace show
at async outereven after anawait?Choose an answer to see the explanation.
Question 6 of 8Your source maps show private code. What should your live website avoid?
Choose an answer to see the explanation.
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.
Question 8 of 8What does the third click print?
Read the code, then predictlet 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
awaitchains, 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, orNODE_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.