JavaScriptIntermediate

JavaScript Error Handling: try, catch, finally and throw

Learn JavaScript error handling with try, catch, finally and throw. Understand error types, create custom errors, and handle problems in real projects.

All JavaScript lessons

What you will learn

Things go wrong in every real program: a user types bad data, a file is missing, the internet drops, a server sends broken text. If you do nothing, JavaScript throws an error and stops running the code. In this lesson you will learn what errors are, the different error types, how to catch them with try...catch, how to clean up with finally, how to create your own errors with throw, how to build custom error classes, and how to use all of this in real projects, so your website shows a friendly message instead of simply breaking.

What happens when there is an error?

console.log("Step 1");

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

console.log("Step 3");    // never runs

The error at the middle line stops the program. “Step 3” is never printed. Error handling lets you decide what happens instead.

The Error object

When something goes wrong, JavaScript creates an Error object with useful information:

Property Meaning
name The type of error, e.g. "TypeError"
message A text explaining what happened
stack The list of function calls that led to the error (great for debugging)
cause Optional: the original error that caused this one
const error = new Error("Something went wrong");

console.log(error.name);      // "Error"
console.log(error.message);   // "Something went wrong"
console.log(typeof error.stack);   // "string"

Common error types

Type When it happens Example
SyntaxError The code or the data is written wrongly JSON.parse("{ bad")
ReferenceError You use a variable that does not exist console.log(missingVariable)
TypeError You use a value in a way its type does not allow null.name, 5()
RangeError A number is outside the allowed range new Array(-1), (1).toFixed(101)
URIError Wrong use of URI functions decodeURIComponent("%")

Let us trigger and catch each one safely:

function tryIt(label, action) {
  try {
    action();
  } catch (error) {
    console.log(`${label}: ${error.name}`);
  }
}

tryIt("Reference", () => console.log(missingVariable));       // ReferenceError
tryIt("Type", () => { const x = null; x.name; });             // TypeError
tryIt("Syntax", () => JSON.parse("{ bad json"));              // SyntaxError
tryIt("Range", () => new Array(-1));                          // RangeError
tryIt("URI", () => decodeURIComponent("%"));                  // URIError

Note: a normal SyntaxError in your own code (like a missing bracket) stops the whole file before it runs, so you cannot catch it. You can only catch syntax errors from things that run later, such as JSON.parse.

try...catch

The idea is simple: try some risky code, and if it throws an error, catch it and handle it.

try {
  // risky code
} catch (error) {
  // runs only if an error was thrown
}
try {
  console.log("Before the error");
  const data = JSON.parse("{ bad json");
  console.log("This line is skipped");
} catch (error) {
  console.log("Something went wrong:", error.message);
}

console.log("The program keeps running!");

How it works:

  1. The code in try runs line by line.
  2. If an error happens, the rest of the try block is skipped.
  3. JavaScript jumps to catch and gives you the error object.
  4. After catch, the program continues normally.

If no error happens, the catch block is skipped.

Optional catch binding

If you do not need the error object, you can leave it out:

try {
  JSON.parse("{ bad");
} catch {
  console.log("Invalid data");
}

Only the code inside try is protected

try {
  const a = 10;
  console.log(a.toUpperCase());   // TypeError, caught
} catch (error) {
  console.log("Caught:", error.name);
}

// An error out here would NOT be caught

Keep the try block small: wrap only the risky lines, not your whole program.

finally

The finally block runs always, whether there was an error or not. It is perfect for cleanup.

function readData(text) {
  console.log("Loading...");

  try {
    return JSON.parse(text);
  } catch (error) {
    console.log("Invalid data");
    return null;
  } finally {
    console.log("Done loading");   // runs in every case
  }
}

readData('{"ok": true}');
// Loading...
// Done loading

readData("{ bad");
// Loading...
// Invalid data
// Done loading

Notice that finally runs even after a return. The return value is kept, and finally runs just before the function really leaves.

Typical uses of finally: hide a loading spinner, close a connection, re-enable a button, release a resource.

You can write try...finally without catch. The error then continues upwards after the finally block runs.

throw: create your own errors

Sometimes the code is valid, but the situation is wrong: a negative age, an empty name, a missing item. You can raise an error yourself with throw.

function divide(a, b) {
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }
  return a / b;
}

console.log(divide(10, 2));   // 5

try {
  divide(10, 0);
} catch (error) {
  console.log(error.message);   // "Cannot divide by zero"
}

When throw runs, the function stops immediately, just like a return, and the error travels up to the nearest catch.

Always throw Error objects

JavaScript allows you to throw anything, but you should always throw an Error object, because it has a stack that shows where the problem started.

// Avoid:
// throw "Something went wrong";
// throw 404;

// Good:
// throw new Error("Something went wrong");

You can also throw the built-in specific types when they fit:

function setAge(age) {
  if (typeof age !== "number") {
    throw new TypeError("Age must be a number");
  }
  if (age < 0 || age > 120) {
    throw new RangeError("Age must be between 0 and 120");
  }
  return age;
}

try {
  setAge("twenty");
} catch (error) {
  console.log(error.name, "-", error.message);   // TypeError - Age must be a number
}

How errors travel up

If a function does not catch an error, it moves up to the function that called it, then to the one that called that, and so on, until something catches it. If nothing does, the program stops.

function level3() {
  throw new Error("Problem in level 3");
}

function level2() {
  level3();
}

function level1() {
  try {
    level2();
  } catch (error) {
    console.log("Caught in level 1:", error.message);
  }
}

level1();   // Caught in level 1: Problem in level 3

This is useful: low-level functions throw, and one place near the top catches and decides what to show the user.

Rethrowing: catch only what you understand

A catch block catches every error, even bugs you did not expect. A good habit: handle the errors you know, and throw the rest again.

function parseAge(text) {
  try {
    const value = JSON.parse(text);
    return value.age;
  } catch (error) {
    if (error instanceof SyntaxError) {
      return null;       // we expected this one
    }
    throw error;         // unknown problem: do not hide it
  }
}

console.log(parseAge('{"age": 22}'));   // 22
console.log(parseAge("{ bad"));         // null

Custom errors

For bigger projects, create your own error types by extending Error. Then you can tell them apart with instanceof.

class ValidationError extends Error {
  constructor(message, field) {
    super(message);              // sets the message
    this.name = "ValidationError";
    this.field = field;          // extra information
  }
}

class NotFoundError extends Error {
  constructor(item) {
    super(`${item} was not found`);
    this.name = "NotFoundError";
  }
}

Using them:

function createUser(name, email) {
  if (!name) throw new ValidationError("Name is required", "name");
  if (!email.includes("@")) throw new ValidationError("Email is not valid", "email");
  return { name, email };
}

try {
  createUser("Riya", "riya-at-example.com");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Problem with "${error.field}": ${error.message}`);
  } else {
    throw error;
  }
}
// Problem with "email": Email is not valid

(Classes are explained fully in the OOP section. For now, copy the pattern: extends Error, call super(message), and set this.name.)

Error cause: keep the original error

When you catch an error and throw a more friendly one, keep the original with the cause option:

function loadSettings(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    throw new Error("Could not load settings", { cause: error });
  }
}

try {
  loadSettings("{ bad");
} catch (error) {
  console.log(error.message);         // "Could not load settings"
  console.log(error.cause.name);      // "SyntaxError"
}

Errors in asynchronous code (a preview)

try...catch works only for code that runs right now. It cannot catch an error that happens later inside a callback:

try {
  setTimeout(() => {
    throw new Error("Too late to catch");
  }, 100);
} catch (error) {
  console.log("This never runs");
}

The error above happens 100 ms later, after the try block has finished. For asynchronous code you will use .catch() on Promises and try...catch with async/await. Here is a small preview:

// You will learn this in the Asynchronous JavaScript lessons
async function loadUser() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users/1");

    if (!response.ok) {
      throw new Error(`Server error: ${response.status}`);
    }

    const user = await response.json();
    console.log(user.name);
  } catch (error) {
    console.log("Could not load the user:", error.message);
  } finally {
    console.log("Request finished");
  }
}

loadUser();

Notice the check on response.ok. fetch does not throw for a 404 or 500 reply. It throws only when the network itself fails. So you must throw the error yourself.

Global safety nets

For errors nobody caught, browsers send an event. You can use it to log the problem (not to hide it):

window.addEventListener("error", (event) => {
  console.log("Uncaught error:", event.message);
});

window.addEventListener("unhandledrejection", (event) => {
  console.log("Unhandled promise error:", event.reason);
});

Real websites send these messages to a logging service so the developer can fix the bug.

Real-life use cases

1. Safe JSON parsing with a fallback

function safeParse(text, fallback = null) {
  try {
    return JSON.parse(text);
  } catch {
    return fallback;
  }
}

console.log(safeParse('{"a": 1}'));        // { a: 1 }
console.log(safeParse("not json", []));    // []

2. Reading from localStorage safely

function getSaved(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    return raw === null ? fallback : JSON.parse(raw);
  } catch {
    return fallback;   // broken data or storage blocked by the browser
  }
}

console.log(getSaved("cart", []));

3. Form validation with clear messages

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function validateSignup({ name, age, password }) {
  if (!name?.trim()) throw new ValidationError("Please enter your name", "name");
  if (!Number.isInteger(age) || age < 13) throw new ValidationError("You must be 13 or older", "age");
  if (!password || password.length < 8) throw new ValidationError("Password needs 8+ characters", "password");
  return true;
}

try {
  validateSignup({ name: "Riya", age: 22, password: "abc" });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`[${error.field}] ${error.message}`);   // [password] Password needs 8+ characters
  }
}

4. Loading indicator with finally

function simulateLoad(shouldFail) {
  const status = { loading: true, error: null };
  console.log("Show spinner");

  try {
    if (shouldFail) throw new Error("Network problem");
    console.log("Show data");
  } catch (error) {
    status.error = error.message;
    console.log("Show error message:", status.error);
  } finally {
    status.loading = false;
    console.log("Hide spinner");   // always hidden, success or failure
  }
}

simulateLoad(false);
simulateLoad(true);

5. Retry an action

function unreliableTask(attempt) {
  if (attempt < 3) throw new Error(`Failed on attempt ${attempt}`);
  return "Success!";
}

function withRetry(task, maxAttempts) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return task(attempt);
    } catch (error) {
      console.log(error.message);
      if (attempt === maxAttempts) throw error;   // give up after the last try
    }
  }
}

console.log(withRetry(unreliableTask, 5));
// Failed on attempt 1
// Failed on attempt 2
// Success!

6. A safe wrapper that returns a result instead of throwing

function attempt(fn) {
  try {
    return { ok: true, value: fn() };
  } catch (error) {
    return { ok: false, error };
  }
}

const result = attempt(() => JSON.parse('{"name":"Riya"}'));

if (result.ok) {
  console.log(result.value.name);   // "Riya"
} else {
  console.log("Failed:", result.error.message);
}

7. Guard clauses: check inputs early

function calculateDiscount(price, percent) {
  if (typeof price !== "number" || Number.isNaN(price)) {
    throw new TypeError("Price must be a number");
  }
  if (percent < 0 || percent > 100) {
    throw new RangeError("Percent must be between 0 and 100");
  }
  return price - (price * percent) / 100;
}

console.log(calculateDiscount(1000, 20));   // 800

8. User-friendly messages

Never show raw technical errors to visitors. Show a friendly message, and log the technical details for yourself:

function showFriendlyError(error) {
  console.error(error);   // technical details for the developer

  if (error instanceof SyntaxError) return "The data we received was not valid.";
  if (error instanceof TypeError) return "Something went wrong. Please try again.";
  return "Unexpected problem. Please try again later.";
}

try {
  JSON.parse("{ bad");
} catch (error) {
  console.log(showFriendlyError(error));   // "The data we received was not valid."
}

Best practices

  • Throw Error objects, not text or numbers.
  • Keep try blocks small. Wrap only the risky lines.
  • Do not leave catch empty. At least log the error with console.error. A silent catch hides bugs.
  • Catch what you can handle. Rethrow the rest.
  • Do not use exceptions for normal flow. Use an if for things you expect often (like an empty search box).
  • Do not rely on the exact message text. It can differ between browsers. Check error.name or instanceof.
  • Use finally for cleanup such as hiding spinners and re-enabling buttons.
  • Show friendly messages to users and keep technical details in the console or your logs.

Common mistakes

  • Putting too much code in one try block. You will not know which line failed.
  • An empty catch {}. The error disappears and the bug becomes very hard to find.
  • Expecting try...catch to catch errors in a setTimeout or other callbacks. Handle the error inside the callback, or use Promises and async/await.
  • Forgetting that fetch does not throw for HTTP errors like 404 and 500. Check response.ok.
  • Throwing text like throw "error". You lose the stack trace and error.message.
  • Forgetting super(message) when creating a custom error class.
  • Forgetting to set this.name in a custom error, so it shows as plain Error.
  • Catching an error and then returning nothing, which makes the rest of the code fail with a confusing undefined.
  • Comparing error.message text to decide what to do. Use instanceof with custom errors instead.
  • Using finally with a return inside it, which overrides the return or error from try and catch. Avoid return in finally.
  • Confusing errors with bugs. try...catch handles problems that can happen even in correct code (bad input, network). It does not replace fixing real bugs.

Practice

  1. Write code that tries to call JSON.parse("{ name: 'Riya' }"), catches the error and prints error.name and a friendly message.
  2. Write a try...catch...finally that prints "start", then an error message, then "cleanup".
  3. Write divide(a, b) that throws an Error when b is 0. Call it inside try...catch.
  4. Write setAge(age) that throws a TypeError if the age is not a number and a RangeError if it is below 0 or above 120.
  5. Create a ValidationError class with a field property. Throw it from a function that checks a name and an email.
  6. In a catch block, handle ValidationError and rethrow every other error.
  7. Write safeParse(text, fallback) and test it with 3 good and 3 bad inputs.
  8. Write a function readNumber(text) that converts text to a number and throws an error when the result is NaN.
  9. Predict the output of a function that has return "try" inside try and console.log("finally") inside finally. Then run it and check.
  10. Challenge: write withRetry(task, attempts) that runs task, retries when it throws, and rethrows the last error after the final attempt.

Recap

  • An uncaught error stops the program. Error handling lets you recover or show a friendly message.
  • An Error object has name, message, stack and optionally cause. Common types: SyntaxError, ReferenceError, TypeError, RangeError, URIError.
  • try { ... } catch (error) { ... } runs risky code and handles any error. Use catch { ... } when you do not need the error object.
  • finally { ... } always runs, even after a return. Use it for cleanup.
  • throw new Error("message") raises an error. Always throw Error objects (or subclasses), never plain text.
  • Errors travel up the call chain until something catches them. Catch what you can handle and rethrow the rest.
  • Create custom errors with class MyError extends Error, calling super(message) and setting this.name. Check them with instanceof.
  • Use { cause: error } to keep the original error inside a new one.
  • try...catch does not catch errors from later callbacks. Use Promises or async/await for asynchronous code, and check response.ok with fetch.
  • Never leave catch empty, and never show raw technical errors to users.
  • Next you will learn strict mode: a safer way to run JavaScript that turns hidden mistakes into clear errors.