JavaScript Error Handling

Handle errors with try/catch and finally, throw and create custom Error types, and catch async errors.

TL;DR

  1. Wrap risky code in try/catch to handle thrown errors cleanly.
  2. Throw custom error classes to make catch blocks more precise.
  3. Use finally to run cleanup code regardless of success or failure.

Try/Catch Basics

    try/catch

    Wrap risky code in a try block to intercept runtime errors.

    try {
      const result = riskyOperation();
      console.log(result);
    } catch (error) {
      console.error('Error:', error.message);
    }
    Catch only runs on error

    The catch block only runs when an error is thrown in try.

    try {
      const data = JSON.parse('invalid');
    } catch (error) {
      console.error('Caught:', error.message); // SyntaxError
    }
    Error object properties

    The error object contains a message, name, and a stack trace.

    catch (error) {
      console.log(error.name);    // "SyntaxError"
      console.log(error.message); // "Unexpected token i"
      console.log(error.stack);   // full trace
    }

Finally Block

    Always runs

    Run cleanup code with finally, which always executes.

    try {
      const file = openFile('data.txt');
      processFile(file);
    } catch (error) {
      console.error('Error:', error);
    } finally {
      closeFile(); // Always runs
    }
    Runs before return

    Finally runs even if catch re-throws or the try block returns early.

    function getData() {
      try {
        return fetchData();
      } finally {
        cleanup(); // runs before function returns
      }
    }
    Resetting UI state

    Finally is useful for resetting loading or spinner state in UIs.

    setLoading(true);
    try {
      await fetchData();
    } finally {
      setLoading(false); // runs on success or failure
    }

Throwing Errors

    throw new Error()

    Throw a new Error with a descriptive message.

    function divide(a, b) {
      if (b === 0) {
        throw new Error('Division by zero');
      }
      return a / b;
    }
    Error objects, not strings

    You can throw any value, but Error objects are best practice.

    // Avoid: throw 'something went wrong';
    // Prefer: throw new Error('something went wrong');
    Built-in error types

    Throw built-in error types for more specific problems.

    function setAge(age) {
      if (typeof age !== 'number') {
        throw new TypeError('Age must be a number');
      }
      if (age < 0 || age > 150) {
        throw new RangeError('Age out of valid range');
      }
    }

Custom Error Classes

    Extending Error

    Create custom error types by extending the built-in Error class.

    class ValidationError extends Error {
      constructor(message) {
        super(message);
        this.name = 'ValidationError';
      }
    }
    instanceof checks

    Check error type with instanceof in catch blocks.

    try {
      if (!email.includes('@')) {
        throw new ValidationError('Invalid email');
      }
    } catch (error) {
      if (error instanceof ValidationError) {
        console.log('Validation error:', error.message);
      }
    }
    Extra properties

    Add extra properties to custom errors for richer context.

    class HttpError extends Error {
      constructor(status, message) {
        super(message);
        this.name = 'HttpError';
        this.status = status;
      }
    }
    throw new HttpError(404, 'Resource not found');

Async Error Handling

    try...catch

    Catches errors thrown during asynchronous operations when combined with await.

    async function loadUser(id) {
      try {
        const res = await fetch(`/users/${id}`);
        return await res.json();
      } catch (err) {
        console.error(err.message);
        return null;
      }
    }
    Multiple awaits

    Groups multiple sequential asynchronous actions into a single error handler block.

    try {
      const user = await getUser();
      const orders = await getOrders(user.id);
      console.log(orders);
    } catch (err) {
      console.error("Chain failed", err);
    }
    finally

    Guarantees resource cleanup or UI state resets regardless of try-catch outcomes.

    setLoading(true);
    try {
      await saveData(form);
    } catch (err) {
      showError(err.message);
    } finally {
      setLoading(false);
    }

Tips

  1. Create custom error classes to identify error types in catch blocks — makes branching logic far clearer than checking messages.
  2. Use finally blocks to release resources like file handles or database connections, since they run regardless of errors.
  3. Re-throw an error after logging it so calling code further up the stack still gets a chance to handle it.

Warnings

  1. Never swallow errors silently with an empty catch block — always log or handle them so bugs don't disappear.
  2. Throwing a plain string instead of an Error object loses the automatic stack trace, making bugs harder to track down.
  3. A return statement inside finally silently overrides any return or thrown error from the try or catch block above it.

In Practice

FAQ