Error Handling in JavaScript

Error handling is a critical aspect of writing robust JavaScript applications. Properly managing errors helps you create more reliable code, improve debugging, and provide better user experiences when things go wrong.

Types of Errors in JavaScript

JavaScript has several built-in error types that you might encounter:

  • SyntaxError: Occurs when there's a mistake in your code syntax
  • ReferenceError: Occurs when referencing a variable that doesn't exist
  • TypeError: Occurs when a value is not of the expected type
  • RangeError: Occurs when a value is not in the expected range
  • URIError: Occurs when using URI handling functions incorrectly
  • EvalError: Occurs when using the eval() function incorrectly
// SyntaxError
if (true {  // Missing closing parenthesis
  console.log("This will cause a syntax error");
}

// ReferenceError
console.log(undefinedVariable);  // Variable doesn't exist

// TypeError
const num = 123;
num.toUpperCase();  // Numbers don't have toUpperCase method

// RangeError
const arr = new Array(-1);  // Cannot create array with negative length

Try...Catch Statement

The try...catch statement is the primary way to handle errors in JavaScript. It allows you to "try" a block of code and "catch" any errors that occur.

try {
  // Code that might throw an error
  const data = JSON.parse('{"name": "John"}');
  console.log(data.name);  // John
} catch (error) {
  // Code to handle the error
  console.error('An error occurred:', error.message);
} finally {
  // Code that will run regardless of whether an error occurred
  console.log('This will always execute');
}

Note: The finally block is optional and will execute regardless of whether an error was thrown or caught.

Catching Specific Error Types

You can check the error type in the catch block to handle different errors differently:

try {
  // Code that might throw different types of errors
  const value = JSON.parse(userInput);
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error('Invalid JSON syntax:', error.message);
  } else if (error instanceof TypeError) {
    console.error('Type error:', error.message);
  } else {
    console.error('Unknown error:', error.message);
  }
}

Throwing Custom Errors

You can throw your own errors using the throw statement. This is useful for creating custom error conditions in your code.

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

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

Creating Custom Error Types

You can create your own error types by extending the built-in Error class:

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

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

// Using custom errors
function fetchUser(id) {
  if (!id) {
    throw new ValidationError('User ID is required');
  }
  
  // Simulate database error
  if (id < 0) {
    throw new DatabaseError('Database connection failed');
  }
  
  return { id, name: 'John Doe' };
}

try {
  const user = fetchUser(-1);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Validation failed:', error.message);
  } else if (error instanceof DatabaseError) {
    console.error('Database error:', error.message);
    // Retry or use fallback
  } else {
    console.error('Unknown error:', error.message);
  }
}

Error Handling in Asynchronous Code

Promises

With Promises, you can use the .catch() method to handle errors:

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json();
  })
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Fetch error:', error.message));

Async/Await

With async/await, you can use try...catch blocks to handle errors in asynchronous code:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    
    const data = await response.json();
    console.log('Data:', data);
    return data;
  } catch (error) {
    console.error('Fetch error:', error.message);
    // You might want to return a default value or re-throw the error
    return null;
  }
}

Best Practice: Always handle errors in your async functions rather than letting them propagate up the call stack unexpectedly.

Error Handling Strategies

Defensive Programming

Anticipate potential errors and handle them before they occur:

function getUserName(user) {
  // Defensive approach
  if (!user) return 'Guest';
  if (typeof user !== 'object') return 'Unknown';
  if (!user.name) return 'Anonymous';
  
  return user.name;
}

// This function is robust against various inputs
console.log(getUserName(null));  // "Guest"
console.log(getUserName(123));   // "Unknown"
console.log(getUserName({}));    // "Anonymous"
console.log(getUserName({ name: 'Alice' }));  // "Alice"

Graceful Degradation

Provide fallback behavior when errors occur:

function loadUserPreferences() {
  try {
    // Try to load from localStorage
    const preferences = JSON.parse(localStorage.getItem('userPrefs'));
    return preferences || getDefaultPreferences();
  } catch (error) {
    console.warn('Could not load preferences:', error.message);
    // Fallback to defaults
    return getDefaultPreferences();
  }
}

function getDefaultPreferences() {
  return {
    theme: 'light',
    fontSize: 'medium',
    notifications: true
  };
}

Interactive Example

Try out this interactive example to see error handling in action:

Best Practices for Error Handling

  • Be specific: Catch specific errors rather than all errors when possible
  • Provide context: Include useful information in error messages
  • Log errors: Log errors for debugging but avoid exposing sensitive details to users
  • Fail gracefully: Provide fallback behavior when errors occur
  • Don't swallow errors: Avoid empty catch blocks that hide errors
  • Clean up resources: Use finally blocks to ensure resources are properly released

Warning: Never use try...catch as a way to control normal program flow. It should be reserved for exceptional conditions.

Next Steps

Now that you understand error handling in JavaScript, you can explore related topics:

  • Debugging techniques in JavaScript
  • Error monitoring and reporting in production
  • Unit testing and error scenarios
  • Error boundaries in React applications