Error Handling in JavaScript

· 12 min read · beginner
error-handling try-catch exceptions debugging
Part 6 of the javascript-fundamentals series

Errors are inevitable in any JavaScript application. Whether it’s a typo in your code, an unexpected API response, or a missing file, your program will encounter situations it wasn’t prepared for. Understanding how to handle these errors gracefully is what separates beginner developers from experienced ones.

In this tutorial, you’ll learn how to use try/catch blocks to handle errors, throw custom errors when something goes wrong, and create robust error-handling strategies for your applications.

Understanding Errors in JavaScript

Before we dive into handling errors, let’s understand what happens when something goes wrong. When JavaScript encounters an error, it throws an exception. If that exception isn’t caught, it propagates up the call stack until it reaches the top level, where it typically crashes your program.

// This will throw an error and crash the program
const result = someUndefinedVariable + 5;
// ReferenceError: someUndefinedVariable is not defined

Errors can occur for many reasons:

  • ReferenceError: Trying to access a variable that doesn’t exist
  • TypeError: Using a value in an unexpected way
  • SyntaxError: Code that violates JavaScript’s grammar
  • RangeError:数值超出允许范围
  • NetworkError: API calls or file operations fail

The Try/Catch Statement

The fundamental way to handle errors in JavaScript is the try/catch statement. Here’s how it works:

try {
  // Code that might throw an error
  const data = JSON.parse(userInput);
  console.log('Parsed successfully:', data);
} catch (error) {
  // Code that runs when an error occurs
  console.log('Failed to parse:', error.message);
}

The code inside the try block executes normally. If an error occurs, JavaScript immediately jumps to the catch block, skipping any remaining code in the try block.

The Error Object

When an error is caught, you receive an Error object with useful properties:

try {
  somethingRisky();
} catch (error) {
  console.log(error.name);    // Error type: "ReferenceError", "TypeError", etc.
  console.log(error.message); // Human-readable description
  console.log(error.stack);   // Stack trace for debugging
}

Try/Catch/Finally

JavaScript provides a third clause: finally. Code in the finally block runs regardless of whether an error occurred:

try {
  const data = fetchData();
  console.log('Data fetched:', data);
} catch (error) {
  console.log('Error fetching data:', error.message);
} finally {
  // This always runs - cleanup code goes here
  console.log('Cleaning up...');
}

The finally block is perfect for cleanup tasks like closing files, releasing resources, or stopping loading indicators.

Throwing Custom Errors

Sometimes you need to create your own errors. Use the throw statement for this:

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

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

You can throw any value, but best practice is to throw Error objects (or subclasses):

// Custom error class
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

function validateAge(age) {
  if (typeof age !== 'number') {
    throw new ValidationError('age', 'Age must be a number');
  }
  if (age < 0 || age > 150) {
    throw new ValidationError('age', 'Age must be between 0 and 150');
  }
}

try {
  validateAge('twenty');
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Validation failed for ${error.field}: ${error.message}`);
  } else {
    throw error; // Re-throw unknown errors
  }
}

Async Error Handling

Handling errors in asynchronous code requires special attention. For Promise-based code, use .catch():

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Fetch failed:', error));

With async/await, wrap your code in try/catch:

async function getData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch:', error);
    return null; // Or re-throw, or throw a custom error
  }
}

Handling Multiple Async Operations

When you have multiple independent async operations, consider handling errors for each individually:

async function loadUserData() {
  const results = await Promise.allSettled([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/settings').then(r => r.json())
  ]);

  results.forEach((result, index) => {
    if (result.status === 'rejected') {
      console.error(`Operation ${index} failed:`, result.reason);
    }
  });
}

Promise.allSettled() waits for all promises to complete (whether successful or failed), giving you the status of each.

Best Practices

Here are some guidelines for effective error handling:

1. Be Specific About Errors

// Bad - catching everything
try {
  riskyOperation();
} catch (error) {
  console.log('Something went wrong');
}

// Good - handling specific errors
try {
  riskyOperation();
} catch (error) {
  if (error instanceof TypeError) {
    // Handle type errors specifically
  } else if (error instanceof RangeError) {
    // Handle range errors specifically
  } else {
    throw error; // Unknown error - let it propagate
  }
}

2. Don’t Swallow Errors

// Bad - hiding errors
try {
  doSomething();
} catch (error) {
  // Silent failure - bug will be hard to find
}

// Good - at minimum, log the error
try {
  doSomething();
} catch (error) {
  console.error('Operation failed:', error);
  // Then handle or re-throw
}

3. Use Error Boundaries (for React)

If you’re building React applications, use error boundaries to catch JavaScript errors in component trees:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

Summary

Error handling is essential for building robust JavaScript applications. Key takeaways:

  • Use try/catch to handle synchronous errors
  • Always include a finally block for cleanup code
  • Throw custom errors with informative messages
  • Handle async errors with .catch() or try/catch in async functions
  • Be specific about which errors you catch
  • Never silently swallow errors - at minimum, log them

In the next tutorial, we’ll explore how to interact with the DOM to build interactive web pages.