Skip to content
aviral gupta

// I4.3 · ~34 min · Intermediate

Stack traces and breakpoints

After this lesson you can find the line where a program failed from its stack trace, add the right console calls, and step through code with a debugger.

Lesson 3 of 5 in I4 Errors, debugging and testing

You will be able to

  • Read a stack trace: the error line, the frame where it happened, and the calls that led there
  • Use console.trace, console.assert and console.error to see what a program does, without mixing it into its output
  • Pause a program with debugger, node inspect or --inspect-brk, and step through it
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson I4.2. What does this print?

    try {
      try {
        null.length;
      } catch (error) {
        throw new Error("Could not count", {cause: error});
      }
    } catch (error) {
      console.log(error.message, "/", error.cause.name);
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. The program crashes. Which functions does the stack trace list in main.js, from the first "at" line down?

    function total(items) {
      return items.reduce((sum, item) => sum + item.price, 0);
    }
    
    function checkout(cart) {
      return total(cart.items);
    }
    
    checkout({item: []});
  3. Practice · Activity 3 of 7

    Fill in the console method that prints a message with the stack at this point.

    console.____("who calls pay?");
    console.("who
  4. Practice · Activity 4 of 7

    In node inspect, match each command to what it does.

  5. Practice · Activity 5 of 7

    The error happens after an await. Which functions does the stack trace list in main.js?

    async function load() {
      await null;
      throw new Error("late");
    }
    
    async function main() {
      await load();
    }
    
    await main();
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The error is made in one function and thrown in another. Which functions does the trace list in main.js?

    function makeError(message) {
      return new Error(message);
    }
    
    function save() {
      throw makeError("disk full");
    }
    
    save();
  7. Apply · Activity 7 of 7

    Mini-task: take a program of yours with a bug, or write report(students), which prints the average of each student’s points but reads s.scores while the data has points. Run it, find the first frame in your file in the stack trace, and fix the line. Add a console.assert that would have caught the problem earlier. Then run it with node inspect, set a breakpoint in average and look at numbers with repl.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A stack trace, read from the program

checkout passes cart.items, but the cart has a typo: item. total then reads reduce from undefined. The program catches the error and prints the first four lines of its stack, with this file’s full URL replaced by main.js: the error, the frame where it happened, the caller, and the top level of the module.

main.js

function total(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

function checkout(cart) {
  return total(cart.items);
}

try {
  checkout({item: [{price: 3}]}); // a typo: item instead of items
} catch (error) {
  // The first four lines of the stack, with this file shown as main.js:
  const lines = error.stack.split("\n").slice(0, 4);
  console.log(lines.join("\n").replaceAll(import.meta.url, "main.js"));
}

Run it with

node main.js

Output

TypeError: Cannot read properties of undefined (reading 'reduce')
    at total (main.js:2:16)
    at checkout (main.js:6:10)
    at main.js:10:3
  • Line 2, column 16 is the dot before reduce: the place where undefined was read.
  • The bug is in the caller’s data, two frames further down: read on until you reach the line that passed the wrong value.
  • error.stack is a plain string; its exact form is V8’s, so do not parse it in production code.

Exercises

Exercise 1 of 2

Follow the trace

orderTotal adds up the order lines; each line is {quantity, product: {name, price: {amount}}}. The tests fail with TypeError: Cannot read properties of undefined (reading 'amount'), and the first frame of the trace is at priceOf (main.js:2:21). Look at that line and the data, and fix it.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    Line 2, column 21 is where .amount is read. Which value before it is undefined?

  2. Hint 2

    Compare line.price with the shape of a line in the tests.

  3. Hint 3

    The price sits inside product: line.product.price.amount.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

function priceOf(line) {
  return line.product.price.amount * line.quantity;
}

export function orderTotal(order) {
  return order.lines.reduce((sum, line) => sum + priceOf(line), 0);
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

function priceOf(line) {
  return line.price.amount * line.quantity;
}

export function orderTotal(order) {
  return order.lines.reduce((sum, line) => sum + priceOf(line), 0);
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {orderTotal} from './main.js';

const order = {
  lines: [
    {quantity: 2, product: {name: 'Tea', price: {amount: 3}}},
    {quantity: 1, product: {name: 'Mug', price: {amount: 8}}}
  ]
};

test('2 teas at 3 and 1 mug at 8 make 14', () => {
  const total = orderTotal(order);
  assert.equal(total, 14, `orderTotal gave ${total}`);
});

test('an empty order is 0', () => {
  assert.equal(orderTotal({lines: []}), 0, 'an empty order should be 0');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

The functions in a stack

stackNames(error) should return the function names of the "at" lines in a V8 stack trace, newest first: for "at total (main.js:2:16)" the name is total, "at async main (…)" is main, and a line without a name in brackets, such as "at main.js:10:3", is left out. The starter also turns the first line, the error message, into a name.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    Run the tests: what does the first name look like?

  2. Hint 2

    Only lines that start with "at " are frames.

  3. Hint 3

    Add line.startsWith("at ") to the filter.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function stackNames(error) {
  return error.stack
    .split("\n")
    .map((line) => line.trim())
    .filter((line) => line.startsWith("at ") && line.includes(" ("))
    .map((line) => line.slice(3).split(" (")[0].replace(/^async /, ""));
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function stackNames(error) {
  return error.stack
    .split("\n")
    .map((line) => line.trim())
    .filter((line) => line.includes(" ("))
    .map((line) => line.slice(3).split(" (")[0].replace(/^async /, ""));
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {stackNames} from './main.js';

test('the names of a written trace, newest first', () => {
  const error = {stack: 'TypeError: x (y) is wrong\n    at total (main.js:2:16)\n    at async main (main.js:6:10)\n    at main.js:10:3'};
  const names = stackNames(error);
  assert.deepEqual(names, ['total', 'main'], `stackNames gave ${JSON.stringify(names)}`);
});

test('a real error starts with the function that made it', () => {
  function inner() {
    return new Error('boom');
  }
  function outer() {
    return inner();
  }
  const names = stackNames(outer());
  assert.deepEqual(names.slice(0, 2), ['inner', 'outer'], `stackNames gave ${JSON.stringify(names)}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Not reading the message

const names = ["ada", "linus"];
for (const name of names) {
  console.log(name.toUppercase());
}

What Node.js prints

TypeError: name.toUppercase is not a function

Why, and the fix

The message already names the problem: name.toUppercase does not exist, so it is undefined, and undefined cannot be called. Look for a typo before you look for anything else: the method is toUpperCase, with a capital C.

A trace that repeats one function

function countDown(n) {
  console.log(n);
  return countDown(n - 1);
}

countDown(3);

What Node.js prints

RangeError: Maximum call stack size exceeded

Why, and the fix

The trace shows countDown again and again: the function calls itself with no end. Every recursive function needs a case that stops: if (n === 0) return;. A trace full of the same frame always points to that.

An error message without context

function setAge(user, text) {
  const age = Number(text);
  if (Number.isNaN(age)) throw new Error("Invalid value");
  user.age = age;
}

setAge({name: "Ada"}, "forty");

What Node.js prints

Error: Invalid value

Why, and the fix

The trace shows where the error was thrown, but the message does not say which value was wrong or why. Put the facts into the message: throw new TypeError(`age must be a number, got "${text}"`). Whoever reads the log, maybe you in six months, can then fix it without a debugger.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

A stack trace, top to bottom

The first line of a stack trace is the error: its name and message. Then come the frames, one "at" line each, newest first: at total (main.js:2:16) means the error happened in the function total, line 2, column 16. The next lines show who called it, back to the top level of the module. Start reading at the first frame in your own files; frames such as node:internal/… belong to Node.js. The format is V8’s; other engines write it differently.

Console calls for debugging

console.trace("here") prints Trace: here and the stack at that point, which shows who called a function. console.assert(condition, message) prints Assertion failed: message only when the condition is false, and the program goes on. Both write to stderr, like console.error, so the program’s real output on stdout stays clean. console.log({total, count}) prints names with the values. Remove these calls once the bug is found.

Breakpoints

A breakpoint pauses the program on a line, so you can look at every variable and go on step by step. Write debugger; on a line: it pauses there when a debugger is attached and does nothing otherwise. node inspect main.js starts Node.js’s terminal debugger: cont goes on to the next breakpoint, next runs the next line, step goes into a function call, repl lets you type expressions. node --inspect-brk main.js waits for Chrome DevTools or your editor to attach.

Sources

Last reviewed October 5, 2026