🐞 Common JavaScript Errors and Solutions
Debugging is detective work — the error message is the clue and the bug is the mystery. The good news: the same handful of errors account for the vast majority of what you'll ever see. Learn to recognize each one on sight, understand why it happens, and you'll fix in seconds what used to take an afternoon.
Week 2 · Day 5 (Friday: Error Handling and Debugging) · Lecture 3
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Distinguish the three broad categories: syntax, runtime, and logical errors
- Read an error message and jump to its cause
- Diagnose and fix
SyntaxError,ReferenceError, and the two most commonTypeErrors - Prevent
RangeErrorfrom runaway recursion and fixconst-reassignment mistakes - Spot classic async pitfalls (missing
await, accidental sequential fetches) - Catch silent logical bugs that never throw — coercion, precision, reference equality, and
this
Estimated Time: 60 minutes
Practice: Fix a buggy calculateAverage and harden an async profile fetcher.
In This Lesson
The Error Hierarchy
Every JavaScript bug falls into one of three families. Knowing which one you're facing tells you where to look.
- Syntax errors — the code can't even be parsed. Caught before anything runs. A misplaced bracket, a stray comma.
- Runtime errors — the code parses fine but throws while executing. Accessing a property of
undefined, calling something that isn't a function. - Logical errors — the code runs without complaint but produces the wrong answer. The sneakiest kind, because nothing throws.
1. SyntaxError: Unexpected Token
Thrown when the engine hits code it can't parse — the program never runs. The message usually points near, but not always exactly at, the offending character.
🐞 The problem
// Missing closing brace on the for-loop
function calculateTotal(items) {
let total = 0;
for (const item of items) {
total += item.price;
// ← the loop's } is missing
return total;
}
// Trailing comma inside JSON text (objects allow it, JSON does not)
const data = JSON.parse('{"name": "John", "age": 30,}');
Uncaught SyntaxError: Unexpected token }
✅ The fix
function calculateTotal(items) {
let total = 0;
for (const item of items) {
total += item.price;
} // ✅ close the loop
return total;
}
// ✅ Valid JSON — no trailing comma
const data = JSON.parse('{"name": "John", "age": 30}');
// TIP: build JSON with stringify instead of hand-typing it
const safe = JSON.stringify({ name: "John", age: 30 });
How to find it fast: a good editor highlights unmatched brackets, and auto-formatting (Prettier) re-indents the file so a missing brace jumps out visually.
2. ReferenceError: Variable Not Defined
You referenced a name that doesn't exist in the current scope — misspelled, out of scope, or used before its let/const declaration (the "temporal dead zone").
🐞 The problem
console.log(username); // used before declaration
let username = "Alice";
let userCount = 10;
console.log(usercount); // typo: lowercase 'c'
function makeLocal() { let localVar = "hidden"; }
console.log(localVar); // out of scope — only exists inside the function
Uncaught ReferenceError: username is not defined
✅ The fix
let username = "Alice";
console.log(username); // declare first, then use
let userCount = 10;
console.log(userCount); // match the exact name
function makeLocal() {
const localVar = "hidden";
return localVar; // return it so callers can use the value
}
console.log(makeLocal());
💡 Modules are strict by default
In an ES module (or with "use strict"), assigning to an undeclared name — myVar = 10; — throws a ReferenceError instead of silently creating a global. That's a feature: it catches typos early.
3. TypeError: Cannot Read Property of Undefined/Null
The single most common runtime error in JavaScript. You tried to read a property from a value that turned out to be undefined or null — often a nested object whose middle layer was missing, or a DOM element that wasn't found.
🐞 The problem
let user; // undefined
console.log(user.name); // 💥
const data = { user: {} }; // no 'name' key
console.log(data.user.name.length); // 💥 name is undefined
const el = document.getElementById("nope"); // null when not found
el.addEventListener("click", handler); // 💥
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
✅ The fix — optional chaining & nullish coalescing
// Optional chaining (?.) short-circuits to undefined instead of throwing
console.log(data.user?.name?.length); // undefined, no crash
// Nullish coalescing (??) supplies a default for null/undefined
const name = user?.name ?? "Guest"; // "Guest"
// Guard DOM lookups — getElementById returns null when nothing matches
const el = document.getElementById("nope");
if (el) {
el.addEventListener("click", handler);
}
💡 Why ?. beats a pile of ifs
Optional chaining stops at the first null/undefined link and returns undefined for the whole expression. It replaces defensive chains like a && a.b && a.b.c with a clean a?.b?.c. Pair it with ?? to supply a fallback.
4. TypeError: X is Not a Function
You tried to call something that isn't callable — a typo'd method name, a variable that was overwritten with a non-function, or a method you forgot to invoke.
🐞 The problem
const numbers = [1, 2, 3];
numbers.maps(n => n * 2); // typo: should be 'map'
let calculate = (a, b) => a + b;
calculate = calculate(5, 3); // now calculate is 8, not a function
calculate(2, 2); // 💥 8 is not a function
const user = { getName() { return this.name; } };
console.log(user.getName); // logs the function, never calls it
Uncaught TypeError: numbers.maps is not a function
✅ The fix
numbers.map(n => n * 2); // correct method name
const calculate = (a, b) => a + b;
const result = calculate(5, 3); // store the RESULT separately
console.log(calculate(2, 2)); // function stays intact
console.log(user.getName()); // ✅ note the parentheses
// Defensive check when a value might not be a function
if (typeof maybeFn === "function") maybeFn();
5. RangeError: Maximum Call Stack Size Exceeded
A recursive function that never reaches a base case keeps calling itself until the call stack overflows. Sometimes it's true infinite recursion; sometimes just recursion that's too deep.
🐞 The problem
function countDown(n) {
console.log(n);
countDown(n - 1); // 💥 no base case — never stops
}
countDown(10);
// An event handler that re-triggers itself
element.addEventListener("click", function () {
element.click(); // 💥 fires this same handler forever
});
Uncaught RangeError: Maximum call stack size exceeded
✅ The fix
// 1) Always give recursion a base case
function countDown(n) {
if (n <= 0) return; // ✅ stopping condition
console.log(n);
countDown(n - 1);
}
// 2) Guard against cycles with a visited set
function walk(node, visited = new Set()) {
if (!node || visited.has(node)) return;
visited.add(node);
walk(node.parent, visited);
}
// 3) When recursion gets deep, iterate instead
function fibonacci(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) [a, b] = [b, a + b];
return b; // O(n), no stack growth
}
6. TypeError: Assignment to Constant Variable
You tried to reassign a const binding. Remember: const locks the binding, not the contents — you can still mutate an object or array it points to.
🐞 The problem
const PI = 3.14159;
PI = 3.14; // 💥 can't rebind a const
const scores = [85, 92, 78];
for (const s of scores) {
s = s + 5; // 💥 s is const each iteration
}
Uncaught TypeError: Assignment to constant variable.
✅ The fix
// Use let when the binding itself must change
let pi = 3.14159;
pi = 3.14; // ✅
// Build a new array instead of reassigning the loop variable
const scores = [85, 92, 78];
const bumped = scores.map(s => s + 5); // [90, 97, 83]
// const objects/arrays can still be MUTATED (contents change, binding doesn't)
const user = { name: "Alice" };
user.name = "Bob"; // ✅ allowed
user.age = 30; // ✅ allowed
// user = {}; // 💥 not allowed — that's a rebind
// Freeze to block mutation too
const frozen = Object.freeze({ name: "Alice" });
frozen.name = "Bob"; // ignored (throws in strict mode)
7. Async/Await Mistakes
Async code has its own family of traps. Most don't throw a loud error — they just give you a Promise where you expected a value, or run slower than they should.
🐞 The problem
async function getData() {
const response = fetch("/api/data"); // 🐞 forgot await → a Promise
const data = response.json(); // 💥 Promise has no .json()
return data;
}
// Independent requests forced to run one-after-another
async function loadAll() {
const user = await fetchUser();
const posts = await fetchPosts(); // waits for user needlessly
const tags = await fetchTags(); // waits for posts needlessly
return { user, posts, tags };
}
TypeError: response.json is not a function
✅ The fix
async function getData() {
const response = await fetch("/api/data"); // ✅ await the fetch
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // await optional on return
}
// Run independent work in parallel with Promise.all
async function loadAll() {
const [user, posts, tags] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchTags()
]); // all three at once
return { user, posts, tags };
}
💡 await sequences, Promise.all parallelizes
Use sequential await only when a later call depends on an earlier result. If the requests are independent, Promise.all fires them together and can cut load time dramatically.
8. Silent Logical Errors
These never throw — the code runs happily and returns the wrong thing. They're found by testing and careful reading, not by error messages.
🐞 The problem
if (x = 5) { } // assignment, not comparison — always truthy
console.log("2" + 2); // "22" — string concatenation, not 4
console.log(0.1 + 0.2 === 0.3); // false — floating-point precision
console.log([1, 2] === [1, 2]); // false — different references
const obj = { value: 42, getValue() { return this.value; } };
const fn = obj.getValue;
console.log(fn()); // undefined — lost 'this' binding
✅ The fix
if (x === 5) { } // strict comparison
console.log(Number("2") + 2); // 4
// Compare floats within a tolerance
const almostEqual = (a, b, eps = 1e-9) => Math.abs(a - b) < eps;
console.log(almostEqual(0.1 + 0.2, 0.3)); // true
// Compare contents, not references
const sameArray = (a, b) =>
a.length === b.length && a.every((v, i) => v === b[i]);
console.log(sameArray([1, 2], [1, 2])); // true
// Preserve 'this' by binding (or use an arrow field in a class)
const fn = obj.getValue.bind(obj);
console.log(fn()); // 42
⚠️ The = vs === classic
A single = inside an if assigns and evaluates to the assigned value — if (x = 5) is always truthy. Modern linters flag this. Reach for === unless you truly mean to assign.
Practice & Quiz
🏋️ Exercise 1: Fix the average
Goal: This function should average [85, 92, 78, 90] to 86.25, but it returns NaN. It has two bugs — find and fix both.
function calculateAverage(numbers) {
let sum; // 🐞 undefined, not 0
for (let i = 0; i <= numbers.length; i++) { // 🐞 off-by-one: <=
sum += numbers[i];
}
return sum / numbers.length;
}
console.log(calculateAverage([85, 92, 78, 90])); // NaN
💡 Hint
Adding to undefined gives NaN. And when i === numbers.length, numbers[i] is undefined — the loop reads one element too far.
✅ Solution
function calculateAverage(numbers) {
let sum = 0; // ✅ initialize to 0
for (let i = 0; i < numbers.length; i++) { // ✅ strictly less than
sum += numbers[i];
}
return sum / numbers.length;
}
console.log(calculateAverage([85, 92, 78, 90])); // 86.25
// Idiomatic alternative:
const average = (nums) => nums.reduce((a, b) => a + b, 0) / nums.length;
🏋️ Exercise 2: Harden the profile fetcher
Goal: Add error handling to this function so a failed request or missing data doesn't crash the caller, and run the two independent requests in parallel.
async function fetchUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
const posts = await fetch(`/api/users/${userId}/posts`);
const userPosts = await posts.json();
return {
user,
posts: userPosts,
totalLikes: userPosts.reduce((sum, p) => sum + p.likes, 0)
};
}
💡 Hint
Wrap it in try/catch, check each response.ok, and use Promise.all since the user and posts requests don't depend on each other.
✅ Solution
async function fetchUserProfile(userId) {
try {
const [userRes, postsRes] = await Promise.all([
fetch(`/api/users/${userId}`),
fetch(`/api/users/${userId}/posts`)
]);
if (!userRes.ok) throw new Error(`User HTTP ${userRes.status}`);
if (!postsRes.ok) throw new Error(`Posts HTTP ${postsRes.status}`);
const user = await userRes.json();
const userPosts = await postsRes.json();
const totalLikes = userPosts.reduce((sum, p) => sum + (p.likes ?? 0), 0);
return { user, posts: userPosts, totalLikes };
} catch (error) {
console.error("Could not load profile:", error.message);
return null; // let the caller decide what to show
}
}
🎯 Quick Quiz
Question 1: console.log(user.name) throws "Cannot read properties of undefined". What's the cleanest guard?
Question 2: Which category of error does NOT throw a message?
Question 3: What most directly causes "Maximum call stack size exceeded"?
Best Practices & Pitfalls
✅ Do
- Read the whole error message and its stack trace before changing anything
- Use
const/let, optional chaining (?.), and nullish coalescing (??) to prevent whole classes of bugs - Run a linter (ESLint) — it catches typos,
=-in-if, and undeclared variables before you do - Validate inputs at function boundaries; guard DOM lookups that can return
null - Give every recursion a base case and every
fetcharesponse.okcheck
❌ Don't
- Compare with
==when you mean===, or assume+means addition - Forget
awaitand then wonder why you got aPromise - Assume nested object properties exist — the middle layer is often the one that's missing
- Use exceptions for expected cases (return
nullfor "not found" instead of throwing) - Compare arrays or objects with
===and expect a value comparison
📖 A systematic debugging process
- Read the error message and note the file & line
- Identify the error type — it narrows the cause immediately
- Reproduce it reliably
- Inspect the variables at the failure point (breakpoint!)
- Form a hypothesis, test the smallest fix, verify
Summary
🎉 Key Takeaways
- Errors are syntax (won't parse), runtime (throws while running), or logical (wrong result, no throw)
- ReferenceError = a name that doesn't exist; TypeError = the wrong type for the operation — the two most common runtime errors
- Optional chaining (
?.) and nullish coalescing (??) prevent the "cannot read property of undefined" crash - Give recursion a base case; remember
constlocks the binding, not the contents - Don't forget
await; parallelize independent requests withPromise.all - The nastiest bugs are logical —
=vs===, coercion, float precision, reference equality, and lostthis
📚 Additional Resources
🚀 What's Next?
You've completed Week 2's deep dive into JavaScript — and now it's time to put it all together. The weekend project is a weather application: you'll call a real public weather API with fetch, handle loading and error states, and render live data to the page.
🎉 Bug-spotting superpowers acquired!
You can now name an error the moment you see it — and you know exactly how to fix it. On to building something real.