EduFlow
JavaScript Fundamentals
0%

Section 5 · Writing Real Programs

Errors and Debugging with DevTools

17 min3 min readCode playground

Lesson at a glance

Read error messages and stack traces, throw and catch errors on purpose with custom error types, and find bugs systematically with breakpoints and the DevTools debugger.

  1. 1Reading an error
  2. 2Throwing errors on purpose
  3. 3The debugger: stop and look
  4. 4A method for any bug

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Every programmer writes bugs every day. What separates experienced developers from beginners is not fewer bugs but a calmer, faster way of finding them. That starts with reading what the computer is telling you.

Reading an error

When JavaScript cannot continue, it throws an error, and if nothing catches it, the console shows something like this:

Text
Uncaught TypeError: Cannot read properties of undefined (reading 'amount')
    at totalCents (expenses.js:12:31)
    at renderSummary (ui.js:40:17)
    at HTMLFormElement.<anonymous> (main.js:27:5)

Read it in three parts. The type, TypeError, says which kind of problem: a value had the wrong type for what you did to it. The message says exactly what: something before .amount was undefined. The stack trace lists the chain of function calls that led there, most recent first: totalCents at line 12 of expenses.js failed, it was called by renderSummary, which was called by the form’s submit handler. In DevTools each location is a link straight to that line.

The types you will meet most:

Type Usually means
ReferenceError A name that does not exist: typo, or used before its declaration
TypeError Wrong kind of value: calling a non-function, reading from undefined
SyntaxError The code cannot be parsed: a missing bracket, quote or comma
RangeError A number outside what is allowed, such as toFixed(200)

Throwing errors on purpose

You can throw errors yourself, and you should when your code is given something it cannot sensibly handle:

JavaScript
function parseAmount(text) {
  const dollars = Number(text);
  if (text.trim() === "" || !Number.isFinite(dollars) || dollars <= 0) {
    throw new Error(`Invalid amount: "${text}"`);
  }
  return Math.round(dollars * 100);
}

try {
  console.log(parseAmount("12.20"));
  console.log(parseAmount("twelve"));
  console.log("this line never runs");
} catch (error) {
  console.log("Caught:", error.message);
} finally {
  console.log("finally always runs");
}

throw stops the function immediately and travels up the call stack until some try/catch catches it. If none does, it becomes an uncaught error in the console. finally runs whether or not anything was thrown, which is the place for clean-up such as hiding a loading spinner.

Custom error types

Some errors are expected, such as a user typing a bad amount; others are bugs. Callers need to tell them apart, and a custom error class makes that possible:

JavaScript
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function addFromForm(label, amountText) {
  if (label.trim() === "") {
    throw new ValidationError("label", "Enter what you spent money on.");
  }
  return { label: label.trim(), amount: Math.round(Number(amountText) * 100) };
}

try {
  addFromForm("  ", "4.50");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Show next to ${error.field}: ${error.message}`);
  } else {
    throw error; // not ours to handle
  }
}

extends Error makes ValidationError a kind of Error with a stack trace, super(message) runs the parent constructor, and the extra field property tells the form where to show the message. The catch handles only what it understands and rethrows everything else, so real bugs still surface.

The debugger: stop and look

console.log is a fine first tool, but it only shows what you thought to print. The DevTools debugger pauses your program on a line and shows you everything.

In Chrome or Edge, open DevTools, go to the Sources panel, open your file and click a line number. That sets a breakpoint. The next time that line is about to run, everything pauses, and you can:

  • hover over any variable to see its value, or read them all in the Scope section;
  • read the Call Stack section to see how you got there;
  • step over (run this line, pause at the next), step into (follow a function call inside), or step out (finish this function);
  • type any expression in the Console while paused, using the paused values.

Right-click a line number to add a conditional breakpoint, such as expense.amount > 100000, which pauses only when the condition is true. You can also write the statement debugger; in your code: when DevTools is open, execution pauses there. Remove it before you commit.

A cycle of five steps: reproduce the bug reliably, read the error or describe the wrong output, form a hypothesis, inspect values with logs or breakpoints, then fix and verify, and repeat if the bug remains. Reproduce Read Hypothesise Inspect Fix and verify still broken: back to reading
A debugging loop that works on every bug

A method for any bug

  1. Reproduce it reliably. “Sometimes the total is wrong” becomes “after deleting the first expense, the total is wrong”.
  2. Read the error, or state precisely what you expected and what you got.
  3. Hypothesise: what single thing would explain it?
  4. Inspect the values at the point you suspect, with a breakpoint or a log, and find the first place where reality differs from your expectation.
  5. Fix the cause there, then verify with the exact steps from step 1.

The temptation, especially when tired, is to skip straight to changing code until the symptom goes away. That usually creates a second bug. Ten minutes of looking at real values beats an hour of guessing.

You now have every piece Pocket needs. Next lesson you assemble them into the finished app.

Key takeaways

  • An error message names the error type, says what went wrong, and its stack trace points to the file and line where it happened.
  • Throw an Error when your code cannot continue sensibly; catch it only where you can do something useful, and rethrow anything you did not expect.
  • A custom error class (class ValidationError extends Error) lets callers tell expected problems apart from real bugs with instanceof.
  • A breakpoint pauses your code on a line so you can inspect every variable and step through it one line at a time; debugger; sets one from code.
  • Debug by evidence, not guesswork; reproduce the bug, find the first line where reality differs from your expectation, then fix that.

Further reading

Check your understanding

3 questions

Question 1 of 3

TypeError: expenses.map is not a function at renderExpenses (ui.js:14). What is the most useful first step?

Question 2 of 3

A catch block in Pocket’s save function reads catch (error) {}. What is the problem?

Question 3 of 3

You want to know why a total is wrong only for the 40th expense in a loop. Which DevTools feature fits best?

Practice

Code playground

Throw what users can fix, rethrow the rest

  1. Finish ValidationError so instances have name set to "ValidationError" and a field property.
  2. Make parseAmount(text) return whole cents, or throw a ValidationError with field "amount" when the text is empty, not a finite number, or not greater than 0.
  3. Fix tryAdd(list, label, amountText). For a valid entry it returns { ok: true, list } with the new expense (trimmed label, amount in cents) added. An empty label throws a ValidationError with field "label". Validation errors become { ok: false, field, message }. Any other error must be rethrown, not swallowed.
// 1. An error type for problems the user can fix
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    // set name and field here
  }
}

// 2. Return whole cents, or throw a ValidationError for the "amount" field
function parseAmount(text) {
  return Math.round(Number(text) * 100);
}

// 3. Return { ok: true, list } with the new expense added, or
//    { ok: false, field, message } for a ValidationError.
//    Any other error must NOT be swallowed.
function tryAdd(list, label, amountText) {
  try {
    const expense = { label: label.trim(), amount: parseAmount(amountText) };
    return { ok: true, list: [...list, expense] };
  } catch (error) {
    return { ok: false };
  }
}

console.log(tryAdd([], "Tea", "abc"));
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Console

(no output)

Checks

  • ValidationError is an Error with a name and a field
  • parseAmount returns whole cents for valid text
  • parseAmount throws a ValidationError for empty, non-numeric, zero, negative or infinite input
  • tryAdd adds a valid expense
  • tryAdd returns { ok: false, field, message } for invalid input
  • tryAdd rethrows errors that are not ValidationErrors