Skip to main content

🐛 Types of Errors in JavaScript

Every program you write will break — that's not a sign of failure, it's part of the job. This lesson maps out the whole landscape of JavaScript errors so that when something goes wrong, you can name it, understand it, and fix it with confidence instead of panic.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Distinguish the three families of errors — syntax, runtime, and logical — and explain when each is detected
  • Identify JavaScript's built-in error types (SyntaxError, ReferenceError, TypeError, RangeError, URIError, AggregateError) and what triggers each
  • Read an error's name, message, and stack to locate a problem quickly
  • Build custom error classes that carry domain-specific context
  • Apply preventive techniques (optional chaining, type guards, strict mode) to stop errors before they happen

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Diagnose a set of broken snippets, then design a small custom error hierarchy.

In This Lesson

Why Errors Are Your Friends

A JavaScript error is the language's way of telling you, precisely and immediately, that it can't do what you asked. Far from being your enemy, an error is the most honest feedback you'll ever get: it names the problem, points at a line, and often hands you a stack trace showing exactly how execution got there.

💡 A useful analogy: Think of errors like the warning lights on a car dashboard. A beginner panics when the check-engine light comes on; a seasoned driver reads which light it is and knows whether to pull over or keep going. Learning the error types is learning to read the dashboard.

The developers who ship reliable software aren't the ones who never see errors — they're the ones who recognize each error on sight and know the standard fix. That recognition is exactly what this lesson builds.

The Three Families of Errors

Before drilling into specific error types, it helps to sort every bug into one of three broad families, distinguished by when they are caught:

graph TD A[JavaScript Errors] --> B[Syntax Errors] A --> C[Runtime Errors] A --> D[Logical Errors] B --> B1["Caught at parse time — code never runs"] C --> C1["Thrown during execution — the program crashes or an exception fires"] D --> D1["Code runs fine but produces the wrong answer"]
FamilyWhen detectedHow you noticeExample
SyntaxBefore execution (parse time)Editor squiggle; nothing runsMissing )
RuntimeDuring executionRed error in the console; script stopsCalling a method on null
LogicalNever automaticallyWrong output; a user reportUsing + where you meant -

The good news: syntax and runtime errors are loud — the engine tells you about them. The tricky one is the logical error, because the code runs happily while doing the wrong thing. We'll give it a dedicated section.

The Built-in Error Hierarchy

When JavaScript throws a runtime error, it throws an object — an instance of the built-in Error constructor or one of its subclasses. Every one of them inherits three key properties, which is why instanceof Error works for all of them.

The JavaScript Error object hierarchy A base Error class at the top branches into SyntaxError, ReferenceError, TypeError, RangeError, URIError, EvalError, and AggregateError. Every subclass inherits the name, message, and stack properties. Error SyntaxError ReferenceError TypeError RangeError URIError EvalError AggregateError Shared properties name · message · stack inherited by every subclass
Figure 1 — Every built-in error extends Error, so they all share name, message, and stack. That shared shape is what lets one catch block handle any of them.

📖 The three properties on every error

name — the error's type as a string, e.g. "TypeError".

message — the human-readable description you write or the engine generates.

stack — a (non-standard but universally supported) trace of the call chain that led to the throw.

The Errors You'll Meet Most

You don't need to memorize all seven subclasses. Three of them account for the overwhelming majority of what you'll debug. Learn these cold.

SyntaxError — the code won't even start

A SyntaxError means the parser couldn't make sense of your code as written. Because it's caught before execution, a single syntax error stops the entire file from running.

// Missing closing parenthesis in the parameter list
function add(a, b {
  return a + b;
}
// Uncaught SyntaxError: Unexpected token '{'

// Invalid JSON handed to JSON.parse — a very common real-world SyntaxError
const raw = '{"name": "John", age: 30}'; // keys must be quoted
JSON.parse(raw);
// Uncaught SyntaxError: Expected property name or '}' in JSON at position 17

Editors and linters (ESLint) catch most syntax errors as you type. The one that still bites experienced developers is invalid JSON from an API — always wrap JSON.parse in a try/catch.

ReferenceError — that name doesn't exist

A ReferenceError fires when you use an identifier that hasn't been declared or isn't reachable in the current scope. The classic cause is a simple typo.

const firstName = 'Ada';
console.log(frstName);  // typo!
// Uncaught ReferenceError: frstName is not defined

// The "temporal dead zone" — let/const exist but aren't usable before their line
console.log(count);     // Uncaught ReferenceError: Cannot access 'count' before initialization
let count = 10;

📖 The Temporal Dead Zone (TDZ)

Variables declared with let and const are hoisted to the top of their block but stay uninitialized until execution reaches the declaration. That gap is the TDZ. Using the variable inside it throws a ReferenceError — unlike var, which quietly gives you undefined. The TDZ is a feature: it turns a silent bug into a loud one.

TypeError — right name, wrong kind of value

A TypeError means a value exists, but it isn't the type the operation needs — most often, trying to read a property or call a method on null or undefined. This is the single most common runtime error in modern JavaScript.

const user = null;
console.log(user.name);
// Uncaught TypeError: Cannot read properties of null (reading 'name')

const total = 42;
total.toUpperCase();
// Uncaught TypeError: total.toUpperCase is not a function

const label = 'locked';
// label = 'open';   // reassigning a const → TypeError: Assignment to constant variable.

RangeError — a valid type, but out of bounds

A RangeError appears when a value is the correct type but outside its permitted range — including the infamous runaway recursion.

const arr = [];
arr.length = -1;               // RangeError: Invalid array length

(123).toPrecision(500);        // RangeError: toPrecision() argument must be between 1 and 100

function loop() { return loop(); }
loop();                        // RangeError: Maximum call stack size exceeded

💡 The rarer two

URIError comes from malformed input to decodeURI / encodeURIComponent and friends. AggregateError (ES2021) bundles several errors into one — you'll see it from Promise.any() when every promise rejects. Both are worth recognizing but rarely worth memorizing.

Logical Errors: The Silent Kind

Logical errors are the hardest to catch precisely because the engine never complains. The code runs, returns a value, and moves on — the value is just wrong. There's no red text to guide you; you find these by testing and by reading carefully.

// Off-by-one: this sums 0..n-1, not 1..n
function sumUpTo(n) {
  let sum = 0;
  for (let i = 0; i < n; i++) sum += i;
  return sum;
}
console.log(sumUpTo(5)); // 10  (expected 15)

// Assignment (=) where comparison (===) was meant
function isAdult(age) {
  if (age = 18) return true; // always assigns 18, which is truthy
  return false;
}
console.log(isAdult(16)); // true  (!)

// Forgot to return
function multiply(a, b) {
  a * b; // computed, then discarded
}
console.log(multiply(2, 3)); // undefined

⚠️ Your defense against logical errors

Because the engine won't help, you build your own safety net: unit tests that assert the expected output, strict equality (===) to avoid coercion surprises, ESLint rules like no-cond-assign that flag if (x = 1), and plain old console logging of intermediate values to see where reality diverges from expectation.

Custom Error Types

The built-in types describe language problems. Your application has its own problems — a failed validation, a missing record, a rejected payment. You can model those precisely by extending Error, which makes your catch blocks able to tell one failure from another with instanceof.

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;            // extra, domain-specific context
  }
}

class ApiError extends Error {
  constructor(message, statusCode, endpoint) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.endpoint = endpoint;
    this.timestamp = new Date();
  }
}

function createAccount(form) {
  if (!form.email?.includes('@')) {
    throw new ValidationError('A valid email is required', 'email');
  }
  // ...
}

try {
  createAccount({ email: 'nope' });
} catch (err) {
  if (err instanceof ValidationError) {
    console.warn(`Fix the "${err.field}" field: ${err.message}`);
  } else {
    throw err; // not ours to handle — let it bubble up
  }
}

For larger apps you can build a small hierarchy so a single catch can handle broad categories:

class AppError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name; // auto-names every subclass
  }
}
class DatabaseError extends AppError {}
class SecurityError extends AppError {}
class AuthError extends SecurityError {} // AuthError is a SecurityError is an AppError

try {
  throw new AuthError('Invalid password');
} catch (err) {
  if (err instanceof SecurityError) console.log('Security issue:', err.message);
  else if (err instanceof AppError) console.log('App issue:', err.message);
}

✅ Custom error checklist

Always extends Error, set a distinct name, attach any context the caller will need (a field, a status code, an id), and consider a base class so you can catch whole families at once.

Preventing Errors Before They Happen

The cheapest error to fix is the one that never fires. Modern JavaScript gives you concise tools to guard against the most common runtime failures.

// Optional chaining (?.) — read deep properties without a TypeError on null/undefined
const city = user?.address?.city;        // undefined instead of a crash

// Nullish coalescing (??) — supply a default only for null/undefined
const port = config.port ?? 3000;        // 0 and '' are kept; null/undefined fall back

// Type guards at the top of a function ("fail fast")
function divide(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('Both arguments must be numbers');
  }
  if (b === 0) throw new RangeError('Cannot divide by zero');
  return a / b;
}

// Strict mode turns silent mistakes into loud ReferenceErrors
'use strict';
undeclared = 5; // ReferenceError instead of quietly creating a global
💡 Consider TypeScript. Once your app grows, a static type checker like TypeScript catches an entire class of TypeErrors and ReferenceErrors before you ever run the code. It's the logical next step after mastering the runtime errors in this lesson.

Hands-on Exercise

🏋️ Part A — Name that error

Objective: For each snippet below, predict which error type (if any) is thrown and why. Run them in your browser console to check.

// 1
const nums = [1, 2, 3];
console.log(nums.at(10).toFixed(2));

// 2
JSON.parse("{'name':'Ray'}");

// 3
const PI = 3.14;
PI = 3.14159;

// 4
console.log(score);
let score = 99;

// 5
function factorial(n) { return n * factorial(n - 1); }
factorial(5);
💡 Hint

Ask two questions of each line: Does it parse? (if not → SyntaxError) and Does the value have the type the operation needs? For #5, notice there's no base case stopping the recursion.

✅ Answers

1. TypeErrornums.at(10) is undefined, and you can't call .toFixed on it.
2. SyntaxError — JSON requires double quotes around keys and strings.
3. TypeError — assignment to a constant variable.
4. ReferenceErrorscore is used inside its temporal dead zone.
5. RangeError — maximum call stack size exceeded (no base case).

🏋️ Part B — Design a small error hierarchy

Objective: Model the failures of a simple online store. Create a base StoreError and two subclasses — OutOfStockError (carrying the productId) and PaymentError (carrying a reason). Write a checkout() that throws the right one, then a try/catch that responds differently to each.

✅ Sample solution
class StoreError extends Error {
  constructor(message) { super(message); this.name = this.constructor.name; }
}
class OutOfStockError extends StoreError {
  constructor(productId) { super(`Product ${productId} is out of stock`); this.productId = productId; }
}
class PaymentError extends StoreError {
  constructor(reason) { super(`Payment failed: ${reason}`); this.reason = reason; }
}

function checkout(cart, card) {
  if (cart.stock === 0) throw new OutOfStockError(cart.id);
  if (!card.valid)      throw new PaymentError('card declined');
  return 'Order placed!';
}

try {
  console.log(checkout({ id: 'A1', stock: 0 }, { valid: true }));
} catch (err) {
  if (err instanceof OutOfStockError) console.log(`Notify: restock ${err.productId}`);
  else if (err instanceof PaymentError) console.log(`Ask user to retry — ${err.reason}`);
  else throw err;
}

🎯 Quick Quiz

Question 1: Which error family is not automatically detected by the JavaScript engine?

Question 2: const user = null; console.log(user.name); throws which error?

Question 3: Why extend the built-in Error class for your own errors?

Summary & Quiz

🎉 Key Takeaways

  • Errors sort into three families by when they're caught: syntax (parse time), runtime (execution), logical (never — you catch these yourself).
  • Every runtime error is an object extending Error, sharing name, message, and stack.
  • The three you'll debug most: SyntaxError (code won't start), ReferenceError (undeclared name / TDZ), and TypeError (wrong kind of value, usually null/undefined).
  • Custom error classes carry domain context and let catch blocks tell failures apart with instanceof.
  • Prevent errors with optional chaining, nullish coalescing, type guards, and strict mode.

📚 Further Reading

🚀 What's Next?

Now that you can name every error, the next lesson teaches you to catch and handle them gracefully with try/catch/finally — turning a crash into a controlled recovery.

🎉 Well done!

You can now read the dashboard. Next up: learning how to respond when the warning lights come on.