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.
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:
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:
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:
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 method for any bug
- Reproduce it reliably. “Sometimes the total is wrong” becomes “after deleting the first expense, the total is wrong”.
- Read the error, or state precisely what you expected and what you got.
- Hypothesise: what single thing would explain it?
- 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.
- 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
Errorwhen 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 withinstanceof. - 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.