JavaScript Error Handling

Handle errors gracefully in JavaScript using try/catch, custom error classes, and finally blocks.

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
    }
    Execution stops at throw

    Code inside try after the thrown line does not execute.

    try {
      throw new Error('stop here');
      console.log('never runs');
    } catch (e) {
      console.log(e.message); // "stop here"
    }
    Optional catch binding

    Omit the catch binding if you don't need the error object.

    try {
      mayFail();
    } catch {
      // optional binding — no variable needed
      console.log('Something went wrong');
    }

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 without an error

    Finally runs even when there is no error in try.

    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
      }
    }
    Releasing resources

    Use finally to release resources like connections or file handles.

    let connection;
    try {
      connection = openDB();
      return connection.query('SELECT * FROM users');
    } finally {
      connection?.close();
    }
    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');
      }
    }
    Re-throwing

    Re-throw errors after logging to let upstream code handle them.

    try {
      riskyOp();
    } catch (e) {
      logger.error(e);
      throw e; // propagate to caller
    }
    Throwing inside catch

    Throwing inside a catch block escalates the error upstream.

    catch (error) {
      if (error instanceof SyntaxError) {
        throw new Error('Config file is malformed');
      }
    }

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');
    Multiple error classes

    Use multiple custom error classes to categorize problems.

    class NetworkError extends Error { }
    class AuthError extends Error { }
    class NotFoundError extends Error { }
    Branching in catch

    Handle specific error types separately, letting unknown errors bubble up.

    catch (error) {
      if (error instanceof AuthError) return redirectToLogin();
      if (error instanceof NetworkError) return showRetry();
      throw error; // unknown errors bubble up
    }

Common Error Types

    SyntaxError

    Occurs when code or data cannot be parsed.

    try {
      JSON.parse('invalid json');
    } catch (error) {
      if (error instanceof SyntaxError) {
        console.log('Invalid JSON format');
      }
    }
    TypeError

    Occurs when a value is used with the wrong type.

    try {
      const x = null;
      x.method(); // TypeError: Cannot read properties of null
    } catch (e) {
      console.log(e instanceof TypeError); // true
    }
    ReferenceError

    Occurs when a variable is not defined.

    try {
      console.log(undeclaredVar);
    } catch (e) {
      console.log(e instanceof ReferenceError); // true
    }
    RangeError

    Occurs when a number falls outside valid bounds.

    try {
      new Array(-1); // RangeError: Invalid array length
    } catch (e) {
      console.log(e instanceof RangeError); // true
    }
    error.name

    Check error names as a string alternative to instanceof.

    catch (error) {
      console.log(error.name); // "TypeError", "RangeError", etc.
      if (error.name === 'TypeError') handleTypeError(error);
    }

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