Errors and Debugging
The four kinds of error you'll actually hit, how to read a stack trace, throwing and catching your own errors, and a repeatable process for finding a bug.
By the end of this lesson, you can
- Distinguish syntax, reference, type, and logic errors, and explain why only the first three announce themselves
- Read a stack trace to find where an error actually originated, not just where it was reported
- Use try/catch/finally to handle an error, and throw a descriptive Error at a validation boundary
- Apply a repeatable process β reproduce, minimize, inspect, hypothesize, test, fix, prevent β to a broken program
Why it matters
Debugging isnβt a separate skill from programming β itβs the same skill, applied to a program that isnβt yet behaving the way you expect. Building a repeatable process now means every future bug is a routine exercise instead of a frustrating mystery.
Four kinds of error
Orientation: Running and Reading JavaScript already
covered SyntaxError (invalid code, caught before anything runs) and ReferenceError (a name that
doesnβt exist). Two more matter just as much:
| Category | Example cause | Announces itself? |
|---|---|---|
| SyntaxError | Mismatched brackets, invalid token | Yes β nothing runs at all |
| ReferenceError | Using a name that was never declared | Yes β thrown when that line runs |
| TypeError | Calling something that isn't a function, reading a property of undefined | Yes β thrown when that line runs |
| Logic error | Off-by-one loop bound, wrong operator, backwards condition | No β the program runs and produces a wrong answer silently |
const total = null;
console.log(total.toFixed(2));
// TypeError: Cannot read properties of null (reading 'toFixed')
Reading a stack trace
A stack trace lists the chain of function calls active when an error was thrown β read top to bottom, the top line is where the error actually happened; lines below it show what called what, in order, to get there:
TypeError: Cannot read properties of undefined (reading 'name')
at getDisplayName (app.js:12:20)
at renderUser (app.js:22:18)
at app.js:30:1
This reads as: getDisplayName (line 12) is where the error happened; it was called by
renderUser (line 22); which was called from the top level (line 30). Even though the reported
crash happens in getDisplayName, the actual bug might be further down the trace β wherever a
user without a name first got passed in.
try/catch/finally and throwing your own errors
try/catch runs a block and, if it throws, catches the error instead of crashing the program;
finally runs regardless of whether an error occurred:
function parseConfig(json) {
try {
return JSON.parse(json);
} catch (error) {
console.error('Invalid config JSON:', error.message);
return {};
} finally {
console.log('Parse attempt finished.');
}
}
You can throw your own errors too β most often at a boundary, where you validate that input matches what the rest of the code assumes:
function withdraw(balance, amount) {
if (amount <= 0) {
throw new Error('Withdrawal amount must be positive.');
}
if (amount > balance) {
throw new Error('Insufficient funds.');
}
return balance - amount;
}
try {
withdraw(100, -5);
} catch (error) {
console.log(error.message); // "Withdrawal amount must be positive."
}
A repeatable debugging process
- Reproduce β get the bug to happen reliably, on demand, before trying to fix anything.
- Minimize β strip away everything not required to reproduce it, down to the smallest example.
- Inspect β read the error, the stack trace, and the actual values involved, not just the symptom.
- Hypothesize β form one specific, testable guess about the cause.
- Test β check the hypothesis directly (a log statement, a breakpoint, a quick script) rather than guessing at a fix.
- Fix β make the smallest change that addresses the confirmed cause.
- Prevent β consider whether a test, a validation check, or a type-narrowing habit would catch this class of bug earlier next time.
Check your understanding
Or reveal the answer without checking
Answer:'caught: boom' then 'cleanup'
risky() throws before reaching console.log('after risky'), so that line never runs. The catch block runs next, logging 'caught: boom', and finally always runs afterward regardless of whether an error occurred.
Or reveal the answer without checking
Answer:Potentially anywhere in A, B, or C β the throw location is where the bad state was finally detected, not necessarily where it originated
The throw location tells you where JavaScript detected an unworkable value β the actual mistake (like passing the wrong data) could be anywhere upstream, in B or C, that led to that value existing. The trace narrows the search; it doesn't always pinpoint the true cause on its own.
Practice: warm-up
Predict the error type for each, then check: null.toString(), undefinedVariable + 1,
JSON.parse('{invalid').
Stuck? Reveal one hint at a time.
Hint 1
Reading a property or calling a method on null/undefined is one specific, common error category.
Hint 2
Referencing a name that was never declared is a different category from the one above.
Hint 3
JSON.parse rejects text that isn't valid JSON β think about which category means "this text never became valid" versus "this ran but hit a bad value."
Reveal the error types
Try the problem yourself before reading this. There is often more than one reasonable approach β treat this as one worked example, not the only correct answer.
null.toString() -> TypeError (cannot read property of null)
undefinedVariable + 1 -> ReferenceError (undefinedVariable is not defined)
JSON.parse('{invalid') -> SyntaxError (invalid JSON text)Practice: apply it
This function has a logic error β it runs without crashing but returns a wrong answer for some inputs. Find it using the debugging process above, then fix it.
function average(numbers) {
let total = 0;
for (let i = 1; i < numbers.length; i++) {
total += numbers[i];
}
return total / numbers.length;
}
console.log(average([2, 4, 6])); // should be 4, but isn't
Stuck? Reveal one hint at a time.
Hint 1
Reproduce it first: run the example and confirm what it actually returns versus what you expect.
Hint 2
Minimize: try an even smaller array, like [10], and see what happens β does it still compute wrong?
Hint 3
Inspect the loop bounds specifically β which index does the loop start from, and which element of the array does that correspond to?
Reveal the bug and fix
Try the problem yourself before reading this. There is often more than one reasonable approach β treat this as one worked example, not the only correct answer.
The loop starts at i = 1, skipping index 0 entirely β numbers[0] (the value 2) never gets
added to total. The fix is starting the loop at i = 0:
function average(numbers) {
let total = 0;
for (let i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total / numbers.length;
}Summary
SyntaxError,ReferenceError, andTypeErrorall announce themselves by throwing. A logic error doesnβt β the program runs and silently produces a wrong result, which is what makes it the hardest category to catch.- A stack traceβs top line is where the error was thrown; the lines below show the chain of calls that led there β the true cause can be anywhere in that chain, not only at the top.
try/catch/finallyhandles an error without crashing the program;finallyalways runs, whether or not an error occurred.- Throwing your own
Errorat a validation boundary stops bad data from silently propagating deeper into the program, where it would cause a more confusing failure later. - Reproduce, minimize, inspect, hypothesize, test, fix, prevent β a repeatable process that turns debugging from guesswork into a routine exercise.