cf.completefrontendCode editorOpen lab
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 only when evidence mattersUse conditional breakpoints, hit counts, event and DOM breakpoints, logpoints, Watch, and the Ignore List deliberately.
  • 02
    Read modern async stacksExplain V8 async stack traces, Error.captureStackTrace, error.cause, console.trace, and stack trace limits.
  • 03
    Debug production-shaped codeMap bundles with source maps, attach Node inspectors, and isolate bugs with reproduction, bisecting, and verification.

Advanced debugging means asking narrower questions

You already know the basics from Debugging in the browser: set a breakpoint, step, read Scope, and add Watch expressions. You also learned in Reading error messages that the top stack frame is evidence, not a verdict. This lesson builds on those skills instead of repeating them.

Definition

Advanced debugging is the habit of narrowing a bug with a reproducible case, targeted pauses, non-invasive traces, async stack evidence, source maps, and runtime-specific tools until one hypothesis can be verified.

The tools are sharper than a plain line breakpoint: conditional breakpoints, hit counts, logpoints, DOM breakpoints, event listener breakpoints, XHR/fetch breakpoints, exception pause, Ignore List, async stack traces, and the Node inspector. The professional move is choosing the smallest tool that answers the current question.

Real-life analogyA flight recorder and a mechanic

A mechanic does not replace random parts. They reproduce the fault, read the recorder, isolate one subsystem, test a theory, and only then change the part. Debugging production JavaScript should feel the same.

In real life: Flight recorder captures the important readings
In JavaScript: Logpoints and traces collect evidence without changing the bug
In real life: Mechanic checks one subsystem at a time
In JavaScript: Conditional breakpoints isolate one state in a loop
In real life: Maintenance manual maps part numbers to real parts
In JavaScript: Source maps connect generated code to authored source
In real life: A supervisor asks for a repeatable failure
In JavaScript: Reproduce, isolate, hypothesize, verify

Where the analogy stops: Real aircraft sensors are installed before the incident. Debuggers can pause and inspect a live program, but pausing can also change timing, especially in async and UI bugs.

You will connect this lesson to try/catch, custom errors, async/await, promise errors, bundlers and source maps, and JavaScript runtimes.

A method before tools

WORKFLOW

Advanced tools are useful only after the bug is repeatable. Use a simple loop: reproduce the failure, isolate the smallest path, hypothesize one cause, verify with an observation, then fix the smallest line and rerun from the start.

  1. Reproduce: write the exact steps, input, browser, Node version, or test command.
  2. Isolate: remove unrelated code, data, requests, or changes until the bug still appears.
  3. Hypothesize: state one falsifiable guess, like “the third click submits twice.”
  4. Verify: pick the least invasive observation: condition, logpoint, Watch expression, trace, or failing test.

Binary search is the same method with math. If a list of changes contains one bad change, test the middle. If the middle fails, search the first half; if it passes, search the second half. Rubber-ducking helps because saying each value aloud often reveals the assumption you never tested.

Bisect a list of changes
Bisect helperPop 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(" | "));
Bisect reportdiscount-formula
first bad changereplace discount formula
checkstest through c-copy: pass | test through e-theme: fail | test through discount-formula: fail
Try it yourself

Binary search found discount-formula after 3 checks instead of reading every change in order.

Real teams use git bisect or feature-flag bisection. The same idea works on smaller lists while you isolate a reproduction.
Keep a debugging journal

Write down the reproduction, what you tried, what you observed, and what changed. It prevents loops like “maybe it is the API” becoming an hour of repeated guesses.

Conditional breakpoints and logpoints

DEVTOOLS

In Chrome DevTools, open the Sources panel, right-click a line number, and choose Add conditional breakpoint... or Add logpoint.... The official UI also includes DOM breakpoints from the Elements panel’s Break on menu, Event Listener Breakpoints, XHR/fetch Breakpoints, and the Pause on exceptions control. Firefox DevTools uses the same names for conditional breakpoints, logpoints, Event Listener Breakpoints, XHR breakpoints, and source maps; it still commonly calls skipped library files blackboxed sources.

Chrome DevTools names to look forJavaScript
// 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

Conditional breakpoints evaluate an expression in the paused line’s scope, such as item.id === 42. Hit counts answer repeated-action bugs: pause on the third click, the tenth timer tick, or the second call from a test. Logpoints print without editing source and without pausing, which is safer when pausing changes timing.

Conditional breakpoints: stop at the interesting item
Step 0 of 10Ready
Your turn: follow the blue line

Step through a loop bug the way a conditional breakpoint works: skip repeated states until the predicate is true, then inspect the matching state.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  { 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(" -> "));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the pause condition

Changing the condition starts a fresh replay. Both conditions hit the same broken item 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.
Break when helper
Conditional break helperPop 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(" -> "));
Recorded stateid
matched item42: monitor
checks before pausechecking 17 -> checking 42
Try it yourself

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

This is not a real debugger. It is a tiny reproduction of the breakpoint question: record the first state that satisfies a predicate.

A logpoint is often the bridge between console.log and a breakpoint. It gives a clue while the program keeps running. The local tracer below is not DevTools, but it demonstrates the same idea: print only when a condition is true, and keep the source behavior otherwise unchanged.

Logpoint-style tracer
Logpoint tracer sourcePop 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. b(60, 0.1) -> 66
  3. 66
Try it yourself

Only the call whose result is greater than 50 emits the trace line, similar to a conditional logpoint.

A real DevTools logpoint is better because it does not wrap or edit source. This local tracer teaches the same observation pattern.
Which breakpoint type fits this bug?
  • A loop has 500 items, but only item.id === 42 is wrong.
  • The bug appears on the third click and not before.
  • You need to print total from a bundled file without changing source.
  • A modal disappears but no one knows which script removed it.
  • A handler registered somewhere responds to every input event.
  • A promise rejection is caught before the Console shows the original line.
  • A checkout request goes to the wrong URL.
Try it yourself
0 of 7 correct

Sort each scenario by the first DevTools feature you would reach for. Several tools can help later; choose the smallest first move.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Similar debugging tools, different costs
ToolUse it whenCost to remember
console.logYou can safely edit local code and want a permanent print while testing.Changes source, can be forgotten, and may not match production bundles.
LogpointYou want Console output from a line without changing the file or pausing.DevTools-only; not a replacement for real logging or telemetry.
Conditional breakpointA line runs many times but only one state matters, such as item.id === 42.The expression itself can have side effects if you write one.
debugger statementA local experiment or hard-to-find generated file needs a temporary pause.Must be removed before sharing; it can surprise anyone with DevTools open.

Async stack traces: follow the await chain

STACKS

A synchronous stack is the current call stack. Older JavaScript engines often stopped at the event loop boundary: after a promise, timer, or callback resumed, the original caller was gone. Modern V8 reconstructs many await chains with “zero-cost async stack traces,” so Node and Chrome can show frames such as at async outer below the throwing function.

Node/V8 async stack probeJavaScript
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"));});

Stack strings are useful but not standardized. V8 writes frames like at async outer (file.js:12:3). Firefox uses a different shape, often outer@file.js:12:3. Treat stacks as human-readable evidence, not a portable data format.

Trim helper frames 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 is V8-specific. It lets a library create an error and omit helper frames above a constructor or factory function. For cross-runtime code, guard it with optional chaining and keep the error useful without it.

Preserve 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);

error.cause connects a high-level error to the lower-level failure without flattening both messages into one string. console.trace() prints the current stack without throwing. Error.stackTraceLimit changes how many V8 frames are collected. These are diagnostic tools, not business logic.

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

Debugging with source maps

MAPPING

A source map connects generated code back to authored code. Bundlers and transpilers may minify, concatenate, or turn TypeScript into JavaScript. The browser runs the generated file, but DevTools can display and breakpoint the original source when it can find the map.

Original TypeScript sourceTypeScript
export function originalName(total: number) {  throw new Error("mapped failure for " + total);} originalName(42);
Generated JavaScript points at a source mapJavaScript
export function originalName(total) {  throw new Error("mapped failure for " + total);}originalName(42);//# sourceMappingURL=checkout.js.map

The comment //# sourceMappingURL=checkout.js.map is the breadcrumb DevTools or Node follows. In Chrome, source maps can be enabled or disabled in Settings Preferences Sources with Enable JavaScript source maps, or through the Command Menu by searching for source maps. If a framework or map marks vendor files as ignored, Chrome’s Ignore List skips them while stepping; many developers still say “blackboxing.”

Where source maps matter
PlaceWhat the map doesDebugging habit
Browser DevToolsMaps minified or transpiled code back to authored source when JavaScript source maps are enabled.Settings > Preferences > Sources > Enable JavaScript source maps, or Command Menu: source map.
Production web buildOften keeps maps private while still uploading them to an error tracker.Use a hidden-source-map style output and do not publish a public sourceMappingURL comment.
Node.jsCan remap stack traces from generated JavaScript to TypeScript or bundled sources.Run with node --enable-source-maps dist/file.js and keep the .map beside the generated file.
Source map commands and production notesbash
# 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.
Security and privacy note

Public source maps can reveal original file names and source. That may be fine for open-source code; it may be wrong for private apps. Many teams upload maps to an error tracker and keep them off the public CDN.

Debugging Node.js

RUNTIME

Node can be debugged with the same inspector protocol Chrome DevTools uses. Start a process with --inspect, open chrome://inspect, and attach. Use --inspect-brk when the bug happens during startup and you need to pause before the first line of user code.

Inspector commands for Nodebash
# 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

VS Code’s JavaScript Debug Terminal automatically launches Node programs with debugging enabled. A launch.json can pin the program, arguments, environment, and source-map behavior for a team. When you only have a terminal, node inspect provides a CLI debugger. For tests, node --test --inspect-brk pauses the built-in test runner before the test file executes.

Node runtime diagnosticsJavaScript
NODE_DEBUG=http,net node server.js node -e "const util = require('node:util'); console.log(util.inspect(obj, { depth: 4 }))" process.on('uncaughtException', (error) => {  console.error(error);  process.exitCode = 1;}); process.on('unhandledRejection', (reason) => {  console.error(reason);  process.exitCode = 1;});

NODE_DEBUG=http,net prints built-in subsystem diagnostics. util.inspect(value, { depth: 4 }) controls how deeply objects are printed. Process handlers for uncaughtException and unhandledRejection are last-resort reporting hooks: log, set a failing exit code, and fix the original error path rather than pretending the process is healthy.

Practical workflows: the third click, loop value, and swallowed rejection

REAL BUGS

Three production-shaped bugs show how the tools combine. A bug that only happens on the third click wants a hit count, event listener breakpoint, or condition. A wrong value inside a loop wants item.id === 42 or a logpoint. A swallowed promise rejection wants exception pause, async stacks, and an error cause chain.

Third-click bug: use a hit count or condition
Step 0 of 15Ready
Your turn: follow the blue line

This replay shows a bug that only appears on the third click. Step until the condition becomes true, then inspect the flag that changed.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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());
CallStoreChangeResultRun = next line. 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.
A swallowed rejection 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));

The bad code returns "saved" even when saveOrder throws. Pause on caught exceptions to stop at the throw site, then fix the path so callers see a meaningful failure and the original cause stays attached.

Wrap the rejection without losing the causePop 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));
Temporary debugger statementJavaScript
function submitForm(form) {  // Temporary local-only pause. Remove it before sharing code.  debugger;  return form.checkValidity();}

A debugger; statement is a real JavaScript pause for local work. It is useful when generated files make line breakpoints hard to place. Do not leave it in shared code; prefer DevTools breakpoints or tests once the bug is understood.

Common misconceptions

“A conditional breakpoint is harmless because it only reads.”

It is just JavaScript. If the expression calls a function with side effects, the debugger can change the program.

“Logpoints are production logging.”

Logpoints are temporary DevTools observations. Production logging needs intentional code, privacy review, and retention rules.

“Async stacks are standardized strings.”

They are engine-specific strings. Assert important function names in tests, not exact file paths or line numbers.

“Source maps fix the bug.”

They only map evidence back to source. You still need to reproduce, isolate, hypothesize, and verify.

“An unhandled rejection handler means the app recovered.”

It means you logged a last-resort failure. Treat the process or request as unhealthy until the original path is fixed.

Debugging habits that are easy to confuse
ConfusionBetter mental modelSafer action
Pause vs observePausing changes timing; observing may be enough.Try a logpoint before pausing animation, timers, or network races.
First frame vs root causeThe top frame threw; a caller may have passed bad data.Read down the stack and preserve error.cause.
Generated file vs authored sourceThe runtime runs generated JavaScript.Use source maps to inspect the source you maintain.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upWrite the condition

A loop only fails for the item whose id is 42. What expression 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 string 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 trace

      Which trace line appears only for the expensive call?

      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

        Run the bisect mentally. Which change label did it find 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 rejection

          Which property should link a wrapper error to the original rejected 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

            These questions mix tool choice with real output. Answer from evidence, not from the tool name alone.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which DevTools action best matches a loop that only fails for item.id === 42?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the small conditional-break helper 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 does a logpoint do that console.log in source does not?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this wrapper error code 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 an async stack include at async outer after an await?

              Choose an answer to see the explanation.

            6. Question 6 of 8What should a production source map strategy avoid when maps contain private source?

              Choose an answer to see the explanation.

            7. Question 7 of 8Which command pauses a Node program before the first line so you can attach a debugger?

              Choose an answer to see the explanation.

            8. Question 8 of 8What does the third-click program print last?

              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 reproduce, isolate, hypothesize, verify; tools support the method.
            • Conditional breakpoints, hit counts, logpoints, DOM breakpoints, event listener breakpoints, XHR/fetch breakpoints, and exception pause answer different questions.
            • Async stack traces reconnect many await chains, but stack strings are engine-specific evidence.
            • Source maps map generated code to authored code; keep production maps private when they expose source.
            • Node debugging includes the inspector, VS Code, node inspect, test debugging, NODE_DEBUG, and careful process-level error handling.

            Remember the one-liner.
            Advanced debugging is not more guessing; it is narrower evidence gathered with the least disruptive tool.

            Up next: Creational patterns.

            CompleteFrontend Clear concepts. Working examples.