JavaScript Error Handling

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

TL;DR

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

Tips

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

Warnings

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

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);
    }

In Practice

FAQ