Errors and Bug Fixing
throw, try,
catch), and a repeatable workflow — so debugging stops being luck and becomes a process.
The five ways code fails
Three runtime errors — and when each fires
When JavaScript cannot continue, it throws an object that describes the problem. Three types cover most of daily life. Every one of these lines runs (this is not a syntax lesson) — try each in isolation and read the message:
// 1. ReferenceError — the name was never declared (or is still in its TDZ)
console.log(username);
// ReferenceError: username is not defined
// 2. TypeError — the value exists, but you asked it for something it cannot do
const count = 3;
console.log(count.toUpperCase());
// TypeError: count.toUpperCase is not a function
// 3. RangeError — the value is the right kind, but outside the allowed range
new Array(-5);
// RangeError: Invalid array length
The distinction matters because it tells you where to look: a ReferenceError
points at a typo or a scope problem; a TypeError points at a value that arrived in
the wrong shape; a RangeError points at input that passed your type checks but is
still invalid.
The fourth kind — SyntaxError — fires before anything runs
// One missing bracket anywhere in the file...
const list = [1, 2, 3;
// SyntaxError: Unexpected token ';'
A SyntaxError is thrown at parse time: none of the script runs,
not even the lines above the mistake. This is why one broken file can take down an entire page —
and why the browser console is the first thing to open when a page appears to do nothing.
The fifth kind — logic errors — throw nothing at all
// Runs fine. Fails silently. This "average" divides by length twice:
const scores = [80, 90, 100];
const average = scores.reduce((sum, s) => sum + s) / scores.length / 2;
console.log(average); // 45 — no error, just a confidently wrong number
Logic errors are the reason tests exist (Phase 5): the machine cannot catch them for you. Only the expected value can.
throw, try and catch
Throwing your own errors — fail loudly, fail early
Any value can be thrown, but throw Error instances only: they carry a
message, a name, and a stack — the three things a
debugger reads:
function divide(a, b) {
// Guard clause: reject impossible input immediately, with a message a human can act on
if (b === 0) {
throw new RangeError(`divide: b must not be 0 (got b=${b})`);
}
return a / b;
}
console.log(divide(10, 4)); // 2.5
console.log(divide(10, 0)); // uncaught RangeError: divide: b must not be 0 (got b=0)
An uncaught error is not a failure of your program — it is your program refusing to continue on false pretences. Silent wrong output is far more expensive than a loud failure.
Catching is for recovery, not for hiding
function loadScore(raw) {
try {
const score = JSON.parse(raw); // can throw SyntaxError on bad JSON
if (typeof score !== "number") {
throw new TypeError("score must be a number"); // our own, deliberate throw
}
return score;
} catch (error) {
console.error("loadScore failed:", error.message); // log it — never vanish silently
return 0; // a fallback the caller can rely on
}
}
console.log(loadScore('{"not":"a number"}'));
// loadScore failed: score must be a number
// 0
console.log(loadScore("42")); // 42
If you see this instead: nothing. An empty catch block is the most
expensive five characters in programming: the program continues in a state nobody planned for,
and the real bug surfaces later, far from its cause. Every catch must do one of three things:
recover, log, or rethrow.
Custom error subclasses — errors you can filter on
When different failures need different handling, subclass Error and branch on the
type instead of on message strings:
class ValidationError extends Error {
constructor(message) {
super(message); // Error's constructor sets .message and .stack
this.name = "ValidationError"; // otherwise it would print as "Error"
}
}
function registerUser(name) {
if (!name || name.length < 2) {
throw new ValidationError(`name must be at least 2 characters (got "${name}")`);
}
return { name };
}
try {
registerUser("");
} catch (error) {
if (error instanceof ValidationError) {
console.log("show the red border:", error.message);
// show the red border: name must be at least 2 characters (got "")
} else {
throw error; // not ours — do not swallow it
}
}A repeatable debug workflow
- Reproduce. A bug you cannot trigger on demand cannot be fixed on purpose.
- Read the error and stack trace first. The top line names the error; the lines below name the path of calls that led there:
TypeError: Cannot read properties of undefined (reading 'role')
at showBadge (user.js:12:15) ← where it broke
at renderHeader (header.js:40:3) ← who called it
at main (app.js:5:1)
The classic beginner mistake is re-reading the code that should work. Read the trace
instead: showBadge received undefined — the question is who passed
it, and the answer is in the frame above.
- Inspect values at the failure point (breakpoints — lesson 18).
- Fix the root cause, not the symptom: a missing check one level up, not a
?.sprinkled at the crash site. - Retest related flows — the same bad value probably reaches other callers.
Defensive coding: validate at the boundary
Inside a function you may trust your own types; at the edges — user input, network responses, JSON files — trust nothing:
function safeUpper(text) {
if (typeof text !== "string") {
// Boundary: the caller may be anyone. Fail fast with a precise message.
throw new TypeError(`safeUpper: expected a string, got ${typeof text}`);
}
return text.toUpperCase();
}
console.log(safeUpper("ok")); // OKWhere try/catch cannot help (yet)
A try/catch only sees errors thrown synchronously inside it.
An error inside a setTimeout callback, or a failed fetch, escapes it
entirely — that failure mode is the subject of Phase 4. For now, know the boundary:
try {
setTimeout(() => {
throw new Error("too late — the catch is long gone");
}, 100);
} catch (error) {
console.log("caught"); // NEVER printed — different turn of the event loop
}
Practice: break it on purpose
The task
Run runtime/22_errors_lab.js with node. It triggers each error type on
purpose, catches them by class, and prints the real message text — predict each line before
running. Then build a parseAge(raw) that throws ValidationError for
negative or non-numeric input, and prove both failure paths with a test call.
The checklist
- You can name the error type you expect from a typo, a bad shape, and bad input.
- You know SyntaxError runs nothing, and logic errors run everything.
- Every catch you write recovers, logs, or rethrows — never hides.
- You can read a three-frame stack trace and say where to look first.