Reading an error like a pro
Every JavaScript error message has the same anatomy, and reading it calmly is half of debugging:
TypeError: Cannot read properties of undefined (reading 'name') at main.js:3
- The name (
TypeError) says what category went wrong. - The message says exactly what happened: something was
undefinedand you asked for its.name. - The stack trace lines point at the file and line number. Start at the top one.
The three you will meet most:
| Error | Usual meaning |
|---|---|
ReferenceError: x is not defined | typo in a name, or the variable is out of scope (lesson 6-3) |
TypeError: ... of undefined | a lookup returned undefined and you kept going |
SyntaxError | the code itself is malformed, often a missing brace or quote |
throw and try/catch
Your own code can refuse bad input by throwing an error, and callers can recover with try/catch. This is Python's raise and try/except mechanism with different keywords, where raise becomes throw and except becomes catch, and the control flow is identical:
function divide(a, b) { if (b === 0) { throw new Error("Cannot divide by zero"); } return a / b; }
A throw stops the function on the spot, and nothing after it in that function runs. If nothing catches the error, the whole program stops with the message you wrote.
A try block volunteers to handle trouble. When anything inside it throws, execution jumps straight to catch, which receives the error object and lets the program carry on. The text you passed to new Error(...) is available there as err.message.
Recovering from a division by zero
The first call succeeds and the second throws. Watching which lines print, and which do not, is the clearest way to see how control jumps.
function divide(a, b) { if (b === 0) { throw new Error("Cannot divide by zero"); } return a / b; } try { console.log(divide(10, 2)); console.log(divide(5, 0)); console.log("never reached"); } catch (err) { console.log("Caught:", err.message); } console.log("program continues");
Output
5
Caught: Cannot divide by zero
program continuesThe line printing never reached lives inside try, directly after the failing call, and it never runs. A throw abandons the rest of the try block entirely rather than skipping one statement, which is worth remembering when a try wraps several steps that depend on each other.
The last line of output is the point of the whole mechanism. Without the try/catch, the error would have ended the program, and program continues would never have appeared.
Guarding a JSON parse
JSON.parse from lesson 5-3 throws whenever the text it receives is not valid JSON, which makes it the most common real reason to reach for try/catch. Data that arrives from elsewhere cannot be trusted to be well formed.
const raw = "{ not valid json }"; try { const person = JSON.parse(raw); console.log(person.name); } catch (err) { console.log("Invalid JSON"); }
Output
Invalid JSON
Both the parse and the use of its result sit inside the try, which is deliberate. Only lines inside the block are protected, so moving console.log(person.name) below the catch would also put person out of scope, since const inside braces stays inside those braces as lesson 6-3 established.
The catch here prints a fallback message and nothing more. That is a legitimate choice when the program has a sensible default, and in a larger program this is where you would log the failure or fall back to previously known data.
Validating a number before trusting it
checkAge throws for anything that is not a usable age and returns the value otherwise. The call passes Number("twenty"), which is NaN as lesson 8-2 showed, so the throw fires.
function checkAge(age) { if (Number.isNaN(age) || age < 0) { throw new Error("Invalid age"); } return age; } try { checkAge(Number("twenty")); console.log("Accepted"); } catch (err) { console.log(err.message); } console.log("Still running");
Output
Invalid age Still running
The two bad cases are joined with ||, so either one is enough to reject the value. Testing for NaN uses Number.isNaN(age) rather than a comparison, because NaN === NaN is false and that check would never fire.
Notice that Accepted never prints while Still running does. The first sits inside the try after the failing call, and the second sits after the whole try/catch, which has already handled the problem by the time control reaches it.
This is the standard shape of input validation: a function that throws with a specific message, and a caller that decides what a failure means.
Diagnosing a ReferenceError
A message reading ReferenceError: userName is not defined means the name itself does not exist anywhere the code can see, and there are two usual causes.
The first is a misspelling, with username and userName being the classic pair, since JavaScript names are case sensitive. The second is a scope problem, where the variable does exist but was declared inside braces that the failing line sits outside of, exactly as lesson 6-3 described.
What this error does not mean is that a variable held nothing. A variable containing null, undefined, or an empty string exists perfectly well, and reading it succeeds quietly. That situation usually surfaces later as a TypeError when a property is read from it.
| Message | The name | Usual fix |
|---|---|---|
ReferenceError: x is not defined | does not exist here | check the spelling, or move the declaration out of the block |
TypeError: Cannot read properties of undefined | exists but holds nothing useful | check what produced the value, often a failed lookup |