Advanced JavaScript

JavaScript Error Handling: Try, Catch, and Error Management

Master error handling in JavaScript. Learn try-catch-finally, custom errors, async error handling, and best practices for robust applications.

By JavaScriptDoc Team
errorstry catcherror handlingdebuggingjavascript

JavaScript Error Handling: Try, Catch, and Error Management

Error handling is crucial for building robust JavaScript applications. Proper error management helps create better user experiences and makes debugging easier.

Understanding JavaScript Errors

JavaScript has several built-in error types that represent different kinds of failures.

// Common error types
console.log(new Error('Generic error'));
console.log(new SyntaxError('Syntax is incorrect'));
console.log(new ReferenceError('Variable not defined'));
console.log(new TypeError('Wrong type'));
console.log(new RangeError('Value out of range'));
console.log(new URIError('URI malformed'));
console.log(new EvalError('Eval error')); // Deprecated

// Error properties
const error = new Error('Something went wrong');
console.log(error.name); // 'Error'
console.log(error.message); // 'Something went wrong'
console.log(error.stack); // Stack trace

Try-Catch-Finally

Basic Try-Catch

// Basic try-catch
try {
  // Code that may throw an error
  const result = riskyOperation();
  console.log('Success:', result);
} catch (error) {
  // Handle the error
  console.error('An error occurred:', error.message);
}

// Catching specific errors
try {
  JSON.parse('invalid json');
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error('Invalid JSON:', error.message);
  } else {
    console.error('Unknown error:', error);
  }
}

// Multiple error scenarios
function divide(a, b) {
  try {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new TypeError('Arguments must be numbers');
    }
    if (b === 0) {
      throw new Error('Division by zero');
    }
    return a / b;
  } catch (error) {
    console.error('Division error:', error.message);
    return null;
  }
}

Finally Block

// Finally always executes
function processFile(filename) {
  let file;

  try {
    file = openFile(filename);
    const data = file.read();
    return processData(data);
  } catch (error) {
    console.error('Error processing file:', error);
    throw error; // Re-throw if needed
  } finally {
    // Always cleanup
    if (file) {
      file.close();
      console.log('File closed');
    }
  }
}

// Finally with return statements
function testFinally() {
  try {
    return 'try';
  } catch (e) {
    return 'catch';
  } finally {
    console.log('finally'); // This runs
    // return 'finally'; // This would override other returns
  }
}

console.log(testFinally()); // Logs: 'finally', Returns: 'try'

Throwing Errors

Basic Error Throwing

// Throwing errors
function validateAge(age) {
  if (age < 0) {
    throw new Error('Age cannot be negative');
  }
  if (age > 150) {
    throw new Error('Age seems unrealistic');
  }
  return age;
}

// Throwing different error types
function processData(data) {
  if (data === null || data === undefined) {
    throw new TypeError('Data cannot be null or undefined');
  }

  if (!Array.isArray(data)) {
    throw new TypeError('Data must be an array');
  }

  if (data.length === 0) {
    throw new RangeError('Data array cannot be empty');
  }

  return data.map((item) => item * 2);
}

// Conditional error throwing
function withdraw(amount, balance) {
  if (amount <= 0) {
    throw new RangeError('Amount must be positive');
  }

  if (amount > balance) {
    throw new Error('Insufficient funds');
  }

  return balance - amount;
}

Custom Error Classes

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

// Custom error with additional properties
class APIError extends Error {
  constructor(message, statusCode, endpoint) {
    super(message);
    this.name = 'APIError';
    this.statusCode = statusCode;
    this.endpoint = endpoint;
    this.timestamp = new Date();
  }
}

// Usage
try {
  throw new APIError('Not Found', 404, '/api/users');
} catch (error) {
  if (error instanceof APIError) {
    console.log(`API Error ${error.statusCode} at ${error.endpoint}`);
  }
}

// Hierarchy of custom errors
class ApplicationError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = 'ApplicationError';
    this.statusCode = statusCode;
  }
}

class UserFacingError extends ApplicationError {
  constructor(message) {
    super(message, 400);
    this.name = 'UserFacingError';
  }
}

class SystemError extends ApplicationError {
  constructor(message) {
    super(message, 500);
    this.name = 'SystemError';
  }
}

Async Error Handling

Promise Error Handling

// Promise rejection handling
fetchUserData(userId)
  .then((user) => {
    console.log('User:', user);
  })
  .catch((error) => {
    console.error('Failed to fetch user:', error);
  });

// Chained error handling
fetchUser(userId)
  .then((user) => fetchPosts(user.id))
  .then((posts) => processPosts(posts))
  .catch((error) => {
    // Catches errors from any step
    console.error('Error in chain:', error);
  });

// Specific error handling in chain
fetchData()
  .then((data) => {
    if (!data) {
      throw new Error('No data received');
    }
    return processData(data);
  })
  .catch((error) => {
    if (error.message === 'No data received') {
      return getDefaultData();
    }
    throw error; // Re-throw other errors
  })
  .then((result) => {
    console.log('Final result:', result);
  })
  .catch((error) => {
    console.error('Unhandled error:', error);
  });

Async/Await Error Handling

// Basic async/await error handling
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Error fetching user:', error);
    throw error; // Re-throw if needed
  }
}

// Multiple async operations
async function processUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchUserPosts(user.id);
    const comments = await fetchPostComments(posts);

    return {
      user,
      posts,
      comments,
    };
  } catch (error) {
    console.error('Error processing user data:', error);
    return null;
  }
}

// Parallel async error handling
async function fetchMultipleUsers(userIds) {
  try {
    const promises = userIds.map((id) => fetchUser(id));
    const users = await Promise.all(promises);
    return users;
  } catch (error) {
    // One failure fails all
    console.error('Failed to fetch users:', error);

    // Alternative: handle individual failures
    const results = await Promise.allSettled(
      userIds.map((id) => fetchUser(id))
    );

    return results
      .filter((result) => result.status === 'fulfilled')
      .map((result) => result.value);
  }
}

Error Handling Patterns

Error Boundaries Pattern

// Error boundary for function calls
function errorBoundary(fn, fallback) {
  return function (...args) {
    try {
      return fn.apply(this, args);
    } catch (error) {
      console.error('Error caught by boundary:', error);

      if (typeof fallback === 'function') {
        return fallback(error);
      }

      return fallback;
    }
  };
}

// Usage
const safeParseJSON = errorBoundary(JSON.parse, (error) => {
  console.error('JSON parse failed:', error);
  return {};
});

const data = safeParseJSON('{"valid": "json"}'); // Works
const invalid = safeParseJSON('invalid json'); // Returns {}

Result Pattern

// Result wrapper pattern
class Result {
  constructor(success, value, error) {
    this.success = success;
    this.value = value;
    this.error = error;
  }

  static ok(value) {
    return new Result(true, value, null);
  }

  static err(error) {
    return new Result(false, null, error);
  }

  isOk() {
    return this.success;
  }

  isErr() {
    return !this.success;
  }

  unwrap() {
    if (this.success) {
      return this.value;
    }
    throw this.error;
  }

  unwrapOr(defaultValue) {
    return this.success ? this.value : defaultValue;
  }
}

// Using Result pattern
function divide(a, b) {
  if (b === 0) {
    return Result.err(new Error('Division by zero'));
  }
  return Result.ok(a / b);
}

const result = divide(10, 2);
if (result.isOk()) {
  console.log('Result:', result.unwrap()); // 5
} else {
  console.error('Error:', result.error.message);
}

Retry Pattern

// Retry with exponential backoff
async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
  let lastError;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      console.error(`Attempt ${i + 1} failed:`, error.message);

      if (i < maxRetries - 1) {
        const delay = initialDelay * Math.pow(2, i);
        console.log(`Retrying in ${delay}ms...`);
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }

  throw lastError;
}

// Usage
const fetchWithRetry = () =>
  retryWithBackoff(
    async () => {
      const response = await fetch('/api/data');
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return response.json();
    },
    5, // max retries
    500 // initial delay
  );

Global Error Handling

Window Error Events

// Global error handler
window.addEventListener('error', (event) => {
  console.error('Global error:', {
    message: event.message,
    filename: event.filename,
    line: event.lineno,
    column: event.colno,
    error: event.error,
  });

  // Send to error tracking service
  trackError({
    message: event.message,
    stack: event.error?.stack,
    url: window.location.href,
    userAgent: navigator.userAgent,
  });
});

// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);

  // Prevent default browser behavior
  event.preventDefault();

  // Handle the error
  handleUnhandledRejection(event.reason);
});

// Promise rejection handling
window.addEventListener('rejectionhandled', (event) => {
  console.log('Rejection handled:', event.reason);
});

Error Reporting Service

class ErrorReporter {
  constructor(endpoint) {
    this.endpoint = endpoint;
    this.queue = [];
    this.setupGlobalHandlers();
  }

  setupGlobalHandlers() {
    // Catch all errors
    window.addEventListener('error', (event) => {
      this.report({
        type: 'error',
        message: event.message,
        stack: event.error?.stack,
        filename: event.filename,
        line: event.lineno,
        column: event.colno,
      });
    });

    // Catch unhandled promise rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.report({
        type: 'unhandledRejection',
        reason: event.reason,
        promise: event.promise,
      });
    });
  }

  report(error) {
    const errorReport = {
      ...error,
      timestamp: new Date().toISOString(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      // Add more context
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight,
      },
    };

    this.queue.push(errorReport);
    this.flush();
  }

  async flush() {
    if (this.queue.length === 0) return;

    const errors = [...this.queue];
    this.queue = [];

    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ errors }),
      });
    } catch (error) {
      // Put errors back in queue
      this.queue.unshift(...errors);
      console.error('Failed to report errors:', error);
    }
  }
}

Error Recovery Strategies

Graceful Degradation

class ResilientComponent {
  constructor() {
    this.errorCount = 0;
    this.maxErrors = 3;
    this.fallbackMode = false;
  }

  async loadData() {
    if (this.fallbackMode) {
      return this.getStaticData();
    }

    try {
      const data = await this.fetchDynamicData();
      this.errorCount = 0; // Reset on success
      return data;
    } catch (error) {
      this.errorCount++;
      console.error(
        `Error loading data (${this.errorCount}/${this.maxErrors}):`,
        error
      );

      if (this.errorCount >= this.maxErrors) {
        console.warn('Switching to fallback mode');
        this.fallbackMode = true;
        return this.getStaticData();
      }

      // Try alternative source
      try {
        return await this.fetchFromCache();
      } catch (cacheError) {
        return this.getStaticData();
      }
    }
  }

  async fetchDynamicData() {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  }

  async fetchFromCache() {
    const cached = localStorage.getItem('cached_data');
    if (!cached) {
      throw new Error('No cached data');
    }
    return JSON.parse(cached);
  }

  getStaticData() {
    return {
      message: 'Using fallback data',
      items: [],
    };
  }
}

Circuit Breaker Pattern

class CircuitBreaker {
  constructor(fn, options = {}) {
    this.fn = fn;
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failures = 0;
    this.nextAttempt = Date.now();
  }

  async call(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }

    try {
      const result = await this.fn(...args);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;

    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
      console.warn('Circuit breaker opened');
    }
  }

  getState() {
    return {
      state: this.state,
      failures: this.failures,
      nextAttempt: this.nextAttempt,
    };
  }
}

// Usage
const protectedAPI = new CircuitBreaker(
  async (endpoint) => {
    const response = await fetch(endpoint);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  },
  {
    failureThreshold: 3,
    resetTimeout: 30000,
  }
);

Debugging and Error Analysis

Enhanced Error Information

class DetailedError extends Error {
  constructor(message, code, details = {}) {
    super(message);
    this.name = 'DetailedError';
    this.code = code;
    this.details = details;
    this.timestamp = new Date();

    // Capture additional context
    this.context = {
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: this.timestamp.toISOString(),
    };

    // Enhance stack trace
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, DetailedError);
    }
  }

  toJSON() {
    return {
      name: this.name,
      message: this.message,
      code: this.code,
      details: this.details,
      context: this.context,
      stack: this.stack,
    };
  }
}

// Usage
throw new DetailedError('Failed to process payment', 'PAYMENT_FAILED', {
  orderId: '12345',
  amount: 99.99,
  currency: 'USD',
  gateway: 'stripe',
  attemptNumber: 3,
});

Error Logging

class ErrorLogger {
  constructor() {
    this.logs = [];
    this.maxLogs = 1000;
  }

  log(error, context = {}) {
    const errorLog = {
      timestamp: new Date().toISOString(),
      message: error.message,
      stack: error.stack,
      type: error.name,
      context,
      // Browser info
      browser: {
        userAgent: navigator.userAgent,
        language: navigator.language,
        platform: navigator.platform,
        cookieEnabled: navigator.cookieEnabled,
      },
      // Page info
      page: {
        url: window.location.href,
        referrer: document.referrer,
        title: document.title,
      },
    };

    this.logs.push(errorLog);

    // Limit log size
    if (this.logs.length > this.maxLogs) {
      this.logs.shift();
    }

    // Also log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.error('Error logged:', errorLog);
    }

    return errorLog;
  }

  getLogs(filter = {}) {
    return this.logs.filter((log) => {
      if (filter.type && log.type !== filter.type) return false;
      if (filter.since && new Date(log.timestamp) < filter.since) return false;
      if (filter.search && !JSON.stringify(log).includes(filter.search))
        return false;
      return true;
    });
  }

  clear() {
    this.logs = [];
  }

  export() {
    return JSON.stringify(this.logs, null, 2);
  }
}

Best Practices

  1. Be specific with error types

    // Good
    if (!user) {
      throw new Error('User not found');
    }
    
    // Better
    if (!user) {
      throw new NotFoundError('User', userId);
    }
    
  2. Always handle async errors

    // Bad
    async function fetchData() {
      const data = await api.getData(); // Unhandled rejection
    }
    
    // Good
    async function fetchData() {
      try {
        const data = await api.getData();
        return data;
      } catch (error) {
        console.error('Failed to fetch data:', error);
        return null;
      }
    }
    
  3. Provide meaningful error messages

    // Bad
    throw new Error('Invalid input');
    
    // Good
    throw new Error(
      `Invalid email format: ${email}. Expected format: user@example.com`
    );
    
  4. Clean up in finally blocks

    let resource;
    try {
      resource = await acquireResource();
      return await processResource(resource);
    } finally {
      if (resource) {
        await releaseResource(resource);
      }
    }
    

Conclusion

Effective error handling is essential for robust JavaScript applications:

  • Try-catch-finally for synchronous error handling
  • Promise catches and async/await for asynchronous errors
  • Custom error classes for specific error types
  • Global error handlers for uncaught errors
  • Error recovery patterns for resilience
  • Proper logging for debugging

Key takeaways:

  • Always handle errors explicitly
  • Use appropriate error types
  • Provide helpful error messages
  • Implement recovery strategies
  • Log errors for debugging
  • Test error scenarios

Master error handling to build reliable, user-friendly JavaScript applications!