Technology · JavaScript

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.

Try/Catch Basics

  • Wrap risky code in try block to intercept runtime errors.
    try {
      const result = riskyOperation();
      console.log(result);
    } catch (error) {
      console.error('Error:', error.message);
    }
  • 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
    }
  • The error object contains a message and a stack trace.
    catch (error) {
      console.log(error.name);    // "SyntaxError"
      console.log(error.message); // "Unexpected token i"
      console.log(error.stack);   // full trace
    }
  • 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"
    }
  • 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

  • Run cleanup code with finally that always executes.
    try {
      const file = openFile('data.txt');
      processFile(file);
    } catch (error) {
      console.error('Error:', error);
    } finally {
      closeFile(); // Always runs
    }
  • Finally runs even when there is no error in try.
  • Finally runs even if catch re-throws or returns early.
    function getData() {
      try {
        return fetchData();
      } finally {
        cleanup(); // runs before function returns
      }
    }
  • Use finally to release resources like connections or file handles.
    let connection;
    try {
      connection = openDB();
      return connection.query('SELECT * FROM users');
    } finally {
      connection?.close();
    }
  • 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 a new Error with a descriptive message.
    function divide(a, b) {
      if (b === 0) {
        throw new Error('Division by zero');
      }
      return a / b;
    }
  • Throw any value, but Error objects are best practice.
    // Avoid: throw 'something went wrong';
    // Prefer: throw new Error('something went wrong');
  • 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-throw errors after logging to let upstream code handle them.
    try {
      riskyOp();
    } catch (e) {
      logger.error(e);
      throw e; // propagate to caller
    }
  • 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

  • Create custom error types by extending the Error class.
    class ValidationError extends Error {
      constructor(message) {
        super(message);
        this.name = 'ValidationError';
      }
    }
  • 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);
      }
    }
  • 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');
  • Use multiple custom error classes to categorize problems.
    class NetworkError extends Error { }
    class AuthError extends Error { }
    class NotFoundError extends Error { }
  • Handle specific error types separately in catch.
    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
    }
  • Check error names as a string alternative to instanceof.
    catch (error) {
      console.log(error.name); // "TypeError", "RangeError", etc.
      if (error.name === 'TypeError') handleTypeError(error);
    }

Tip: Create custom error classes to identify error types in catch blocks precisely — makes branching logic far clearer than checking messages.

Warning: Never swallow errors silently with an empty catch block — always log or handle them so bugs do not disappear unnoticed.

JavaScript DestructuringJavaScript Events