Skip to main content

🐞 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 common TypeErrors
  • Prevent RangeError from runaway recursion and fix const-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.
graph TD A[JavaScript Errors] --> B[Syntax Errors] A --> C[Runtime Errors] A --> D[Logical Errors] B --> B1[Unexpected token] B --> B2[Missing bracket/paren] C --> C1[ReferenceError: not defined] C --> C2[TypeError: cannot read property] C --> C3[RangeError: stack exceeded] D --> D1[Wrong result, no throw] D --> D2[Off-by-one, bad comparison]

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 fetch a response.ok check

❌ Don't

  • Compare with == when you mean ===, or assume + means addition
  • Forget await and then wonder why you got a Promise
  • Assume nested object properties exist — the middle layer is often the one that's missing
  • Use exceptions for expected cases (return null for "not found" instead of throwing)
  • Compare arrays or objects with === and expect a value comparison

📖 A systematic debugging process

  1. Read the error message and note the file & line
  2. Identify the error type — it narrows the cause immediately
  3. Reproduce it reliably
  4. Inspect the variables at the failure point (breakpoint!)
  5. 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 const locks the binding, not the contents
  • Don't forget await; parallelize independent requests with Promise.all
  • The nastiest bugs are logical= vs ===, coercion, float precision, reference equality, and lost this

📚 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.