Skip to content
Educora
Advanced22 min20 / 27

Error handling and debugging

Go beyond `try...catch`: built-in error types, custom error classes with `cause`, reading stack traces, the `debugger` statement and console methods that make bugs easy to find.

Check yourself
In this lesson you will learn
  • Recognise the built-in error types (TypeError, ReferenceError, RangeError, SyntaxError) and their usual causes
  • Create custom error classes, add a cause and handle errors by type, including in async code
  • Read a stack trace and debug with breakpoints, debugger and console methods

A user writes: “The button does nothing.” There is no sign on the screen, but the console has a red line: in Chrome, TypeError: Cannot read properties of undefined (reading 'price'), followed by file names and numbers. For a beginner it is noise; for a professional it is a map pointing straight at the bug. In this lesson you will learn to read that map and to design your own errors well.

Built-in error types

TypeUsual causeExample
TypeErroran operation on a value of the wrong type: calling a non-function, reading a property of undefined/nullnull.name, (42)()
ReferenceErrora variable that does not exist (or is in the TDZ)console.log(missing)
RangeErrora number outside the allowed rangenew Array(-1)
SyntaxErrorcode or JSON that cannot be parsedJSON.parse('{bad}')
JavaScript
const attempts = [
  () => null.name,
  () => missingVariable,
  () => new Array(-1),
  () => JSON.parse('{bad json}'),
  () => (42)(),
];

for (const attempt of attempts) {
  try {
    attempt();
  } catch (error) {
    console.log(error.name, '|', error instanceof Error);
  }
}
▸ Expected output
TypeError | true
ReferenceError | true
RangeError | true
SyntaxError | true
TypeError | true

Your own errors and cause

throw can throw any value, but always throw an Error object — only it has a stack trace. A good rule: catch only the errors you can handle and rethrow the rest (throw error). To tell your program's errors apart, build a hierarchy of classes that extend Error — then instanceof lets you react to each kind properly.

JavaScript
class AppError extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = this.constructor.name;
  }
}
class ValidationError extends AppError {}
class NotFoundError extends AppError {}

function findUser(users, id) {
  if (!Number.isInteger(id)) throw new ValidationError(`Bad id: ${id}`);
  const user = users.find((u) => u.id === id);
  if (!user) throw new NotFoundError(`User ${id} not found`);
  return user;
}

const users = [{ id: 1, name: 'Aysel' }];
for (const id of [1, 'x', 7]) {
  try {
    console.log(findUser(users, id).name);
  } catch (error) {
    if (error instanceof ValidationError) console.log('400', error.message);
    else if (error instanceof NotFoundError) console.log('404', error.message);
    else throw error;
  }
}
▸ Expected output
Aysel
400 Bad id: x
404 User 7 not found
this.constructor.name gives every subclass the right name automatically. 400 and 404 are HTTP status codes; we will come back to them in the API lesson.

When you catch a low-level error and throw a clearer one, keep the original in the **cause** property (ES2022). This way the user sees a clear message and the developer does not lose the real reason.

JavaScript
function loadSettings(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    throw new Error('Settings file is broken', { cause: error });
  }
}

try {
  loadSettings('{ theme: dark }');
} catch (error) {
  console.log(error.message);
  console.log('Caused by:', error.cause.name);
  console.log(error.cause instanceof SyntaxError);
}
▸ Expected output
Settings file is broken
Caused by: SyntaxError
true
JavaScript
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function saveScore(score) {
  await wait(100);
  if (score < 0) throw new RangeError('Score cannot be negative');
  return 'saved';
}

try {
  console.log(await saveScore(10));
  console.log(await saveScore(-5));
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

const result = await saveScore(-1).catch((error) => `fallback (${error.message})`);
console.log(result);
▸ Expected output
saved
RangeError: Score cannot be negative
fallback (Score cannot be negative)

Reading a stack trace

A stack trace shows the chain of calls at the moment the error was created, newest on top. The first line is the error's name and message; the following lines look like at function (file:line:column). In the cart.js file below, the second item has no product property. This is the trace Node.js prints (folder paths shortened, Node's internal lines left out):

JavaScript
// cart.js
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.product.price * item.qty, 0);
}
function renderCart(cart) {
  return calculateTotal(cart.items);
}
renderCart({ items: [{ product: { price: 2 }, qty: 1 }, { qty: 3 }] });
Expected output
TypeError: Cannot read properties of undefined (reading 'price')
    at cart.js:3:57
    at Array.reduce (<anonymous>)
    at calculateTotal (cart.js:3:16)
    at renderCart (cart.js:6:10)
    at Object.<anonymous> (cart.js:8:1)

Debugging tools

  1. 1
    Reproduce the bug

    Find the exact steps that lead to the bug.

  2. 2
    Set a breakpoint

    In DevTools › Sources, click a line number, or write debugger; in the code — execution will stop there while DevTools is open.

  3. 3
    Inspect the values

    Look at the variables in Scope, hover over values, add expressions to Watch.

  4. 4
    Step through

    Move line by line, step into a function or step out of it.

  5. 5
    Fix and verify

    After the fix, repeat the same steps again.

open DevToolsF12
resume executionF8
step over to the next lineF10
step into a functionF11
step out of a functionShift+F11

The console has more useful methods than log. The runner on this page shows only console.log, so run this example in the browser console or in Node.js. The output below comes from Node.js; the time measured for sort differs on every run.

JavaScript
const students = [
  { name: 'Aysel', score: 92 },
  { name: 'Murad', score: 78 },
];

console.table(students);

console.group('Checkout');
console.log('items: 3');
console.warn('coupon expired');
console.groupEnd();

console.count('click');
console.count('click');
console.assert(students.length > 5, 'Expected more than 5 students');

console.time('sort');
students.sort((a, b) => b.score - a.score);
console.timeEnd('sort');
Expected output
┌─────────┬─────────┬───────┐
│ (index) │ name    │ score │
├─────────┼─────────┼───────┤
│ 0       │ 'Aysel' │ 92    │
│ 1       │ 'Murad' │ 78    │
└─────────┴─────────┴───────┘
Checkout
  items: 3
  coupon expired
click: 1
click: 2
Assertion failed: Expected more than 5 students
sort: 0.027ms
Exercise

Complete the ValidationError class (its name must be 'ValidationError') and write parseAge(text). If the text is not a whole number, throw a ValidationError with the message Not a whole number: <text>; if it is outside 0–120, use Out of range: <age>; otherwise return the age.

Exercise · JavaScript
class ValidationError extends Error {
  // set this.name in the constructor
}

function parseAge(text) {
  // Number(text) must be an integer from 0 to 120
}

for (const input of ['16', 'abc', '130', '42']) {
  try {
    console.log('Age:', parseAge(input));
  } catch (error) {
    if (error instanceof ValidationError) {
      console.log(`${error.name}: ${error.message}`);
    } else {
      throw error;
    }
  }
}
▸ Expected output
Age: 16
ValidationError: Not a whole number: abc
ValidationError: Out of range: 130
Age: 42
Exercise

Write retry(fn, times): it calls fn at most times times until it succeeds and prints Retrying after: <message> after each failure. If every attempt fails, it throws an error with the message All <times> attempts failed that keeps the last error in cause.

Exercise · JavaScript
function createFlakyTask(failuresBeforeSuccess) {
  let calls = 0;
  return () => {
    calls++;
    if (calls <= failuresBeforeSuccess) throw new Error(`Attempt ${calls} failed`);
    return `Success on attempt ${calls}`;
  };
}

function retry(fn, times) {
  // your code here
}

console.log(retry(createFlakyTask(2), 3));
try {
  retry(createFlakyTask(5), 3);
} catch (error) {
  console.log(error.message, '| cause:', error.cause.message);
}
▸ Expected output
Retrying after: Attempt 1 failed
Retrying after: Attempt 2 failed
Success on attempt 3
Retrying after: Attempt 1 failed
Retrying after: Attempt 2 failed
Retrying after: Attempt 3 failed
All 3 attempts failed | cause: Attempt 3 failed

Key points

  • TypeError means a wrong type or undefined, ReferenceError an unknown variable, RangeError a value out of range, SyntaxError code or JSON that cannot be parsed.
  • Check error.name or instanceof, not the message text — messages vary between browsers.
  • Custom error classes and cause make errors meaningful and keep the original reason.
  • An async error is caught only with await inside try or with .catch().
  • Read a stack trace from the first line with your own file; use breakpoints, debugger and console methods instead of guessing.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
Which error does const user = null; user.length; throw?