Labs ICT
Pro Login

Try / Catch

Try-catch is JavaScript's fundamental error handling mechanism that allows developers to gracefully handle runtime errors and prevent application crashes.

In this comprehensive tutorial, we'll explore try-catch statements, error types, best practices, and advanced error handling patterns in JavaScript.

Try-Catch Basics

The try-catch statement consists of a try block that contains code that might throw an error, and a catch block that handles the error if one occurs.

Basic Try-Catch Syntax

// Basic try-catch syntax
try {
  // Code that might throw an error
} catch (error) {
  // Code to handle the error
}

// Simple example
try {
  const result = 10 / 0;
  console.log(result); // Infinity (no error)
} catch (error) {
  console.error('An error occurred:', error.message);
}

// Example with actual error
try {
  const result = undefinedVariable.property; // Throws ReferenceError
} catch (error) {
  console.error('Error caught:', error.message);
  console.error('Error type:', error.name);
}

// Example with JSON parsing
try {
  const invalidJson = '{ "name": "John", age: 30 }'; // Invalid JSON
  const parsed = JSON.parse(invalidJson);
  console.log(parsed);
} catch (error) {
  console.error('JSON parsing error:', error.message);
}

// Example with function that throws error
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('Division error:', error.message);
}

// Try-catch with return statement
function safeDivide(a, b) {
  try {
    return a / b;
  } catch (error) {
    console.error('Division failed:', error.message);
    return null; // Return null on error
  }
}

const result1 = safeDivide(10, 2);
const result2 = safeDivide(10, 0);
console.log(result1); // 5
console.log(result2); // null

Try-Catch-Finally

// Try-catch-finally syntax
try {
  // Code that might throw an error
} catch (error) {
  // Code to handle the error
} finally {
  // Code that always executes
}

// Example with finally block
function processFile(filename) {
  let fileContent = null;
  
  try {
    // Simulate file reading
    if (filename === 'nonexistent.txt') {
      throw new Error('File not found');
    }
    fileContent = 'File content here';
    console.log('File processed successfully');
    return fileContent;
  } catch (error) {
    console.error('File processing error:', error.message);
    return null;
  } finally {
    console.log('Cleanup: Closing file resources');
    // This always executes, regardless of error
  }
}

processFile('data.txt'); // Success case
processFile('nonexistent.txt'); // Error case

// Finally block executes even with return
function testFinally() {
  try {
    console.log('In try block');
    return 'Return from try';
  } catch (error) {
    console.log('In catch block');
    return 'Return from catch';
  } finally {
    console.log('In finally block');
    // This executes before return
  }
}

const result = testFinally();
console.log(result);

// Finally block with throw
function testFinallyWithThrow() {
  try {
    console.log('In try block');
    throw new Error('Something went wrong');
  } catch (error) {
    console.log('In catch block:', error.message);
    return 'Handled error';
  } finally {
    console.log('In finally block');
    // Still executes even though catch handled the error
  }
}

const result2 = testFinallyWithThrow();
console.log(result2);

// Finally without catch
function tryFinallyOnly() {
  try {
    console.log('In try block');
    // Some operation that might throw
    return 'Success';
  } finally {
    console.log('In finally block');
    // Cleanup code here
  }
}

const result3 = tryFinallyOnly();
console.log(result3);

Error Object Properties

// Error object properties
try {
  throw new Error('Custom error message');
} catch (error) {
  console.log('Error name:', error.name); // 'Error'
  console.log('Error message:', error.message); // 'Custom error message'
  console.log('Error stack:', error.stack); // Stack trace
  console.log('Error toString():', error.toString()); // 'Error: Custom error message'
}

// Different error types
try {
  // ReferenceError
  const result = undefinedVariable.property;
} catch (error) {
  console.log('Error type:', error.constructor.name); // 'ReferenceError'
  console.log('Error name:', error.name); // 'ReferenceError'
  console.log('Error message:', error.message);
}

try {
  // TypeError
  const obj = null;
  const result = obj.property;
} catch (error) {
  console.log('Error type:', error.constructor.name); // 'TypeError'
  console.log('Error name:', error.name); // 'TypeError'
  console.log('Error message:', error.message);
}

try {
  // SyntaxError
  // eval('const x = ;'); // This would throw SyntaxError
  throw new SyntaxError('Invalid syntax');
} catch (error) {
  console.log('Error type:', error.constructor.name); // 'SyntaxError'
  console.log('Error name:', error.name); // 'SyntaxError'
  console.log('Error message:', error.message);
}

try {
  // RangeError
  const arr = new Array(-1); // Invalid array length
} catch (error) {
  console.log('Error type:', error.constructor.name); // 'RangeError'
  console.log('Error name:', error.name); // 'RangeError'
  console.log('Error message:', error.message);
}

// Custom error properties
class CustomError extends Error {
  constructor(message, code, details) {
    super(message);
    this.name = 'CustomError';
    this.code = code;
    this.details = details;
    this.timestamp = new Date();
  }
}

try {
  throw new CustomError('Something went wrong', 'ERR_001', { userId: 123 });
} catch (error) {
  console.log('Error name:', error.name); // 'CustomError'
  console.log('Error message:', error.message); // 'Something went wrong'
  console.log('Error code:', error.code); // 'ERR_001'
  console.log('Error details:', error.details); // { userId: 123 }
  console.log('Error timestamp:', error.timestamp);
}

Error Types

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

Built-in Error Types

// Error - Base error class
try {
  throw new Error('Generic error');
} catch (error) {
  console.log('Error:', error.name, error.message);
}

// ReferenceError - When trying to access undefined variable
try {
  console.log(undefinedVariable);
} catch (error) {
  console.log('ReferenceError:', error.message);
}

// TypeError - When operation is performed on wrong type
try {
  const num = 42;
  num(); // Trying to call number as function
} catch (error) {
  console.log('TypeError:', error.message);
}

// SyntaxError - When parsing invalid JavaScript code
try {
  eval('const x = ;'); // Invalid syntax
} catch (error) {
  console.log('SyntaxError:', error.message);
}

// RangeError - When numeric value is outside allowed range
try {
  const arr = new Array(Number.MAX_SAFE_INTEGER);
} catch (error) {
  console.log('RangeError:', error.message);
}

// URIError - When URI manipulation functions are used incorrectly
try {
  decodeURIComponent('%'); // Invalid URI component
} catch (error) {
  console.log('URIError:', error.message);
}

// EvalError - Historically used for eval-related errors (rarely used now)
try {
  throw new EvalError('Eval error example');
} catch (error) {
  console.log('EvalError:', error.message);
}

// AggregateError - Multiple errors in one (ES2021)
try {
  const errors = [
    new Error('First error'),
    new Error('Second error'),
    new Error('Third error')
  ];
  throw new AggregateError(errors, 'Multiple errors occurred');
} catch (error) {
  console.log('AggregateError:', error.message);
  console.log('Individual errors:');
  error.errors.forEach((err, index) => {
    console.log(`  ${index + 1}. ${err.message}`);
  });
}

// InternalError - Indicates an error in the JavaScript engine (rare)
try {
  throw new InternalError('Internal error example');
} catch (error) {
  console.log('InternalError:', error.message);
}

Custom Error Classes

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

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

class NetworkError extends Error {
  constructor(message, statusCode, url) {
    super(message);
    this.name = 'NetworkError';
    this.statusCode = statusCode;
    this.url = url;
  }
}

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

// Using custom errors
function validateEmail(email) {
  if (!email) {
    throw new ValidationError('Email is required', 'email');
  }
  if (!email.includes('@')) {
    throw new ValidationError('Invalid email format', 'email');
  }
  return true;
}

function authenticateUser(username, password) {
  if (!username || !password) {
    throw new AuthenticationError('Username and password required', username);
  }
  if (username !== 'admin' || password !== 'secret') {
    throw new AuthenticationError('Invalid credentials', username);
  }
  return { id: 1, name: 'Admin User' };
}

function makeApiRequest(url) {
  if (url === 'https://api.example.com/error') {
    throw new NetworkError('API request failed', 500, url);
  }
  return { data: 'success' };
}

function executeQuery(query, params) {
  if (query.includes('DROP')) {
    throw new DatabaseError('Dangerous query detected', query, params);
  }
  return { rows: [] };
}

// Testing custom errors
try {
  validateEmail('invalid-email');
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Validation failed for field '${error.field}': ${error.message}`);
  }
}

try {
  authenticateUser('user', 'wrong');
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.log(`Authentication failed for user '${error.userId}': ${error.message}`);
  }
}

try {
  makeApiRequest('https://api.example.com/error');
} catch (error) {
  if (error instanceof NetworkError) {
    console.log(`Network error: ${error.message} (Status: ${error.statusCode}, URL: ${error.url})`);
  }
}

try {
  executeQuery('DROP TABLE users', []);
} catch (error) {
  if (error instanceof DatabaseError) {
    console.log(`Database error: ${error.message} (Query: ${error.query})`);
  }
}

// Error hierarchy and instanceof checking
class AppError extends Error {
  constructor(message, code) {
    super(message);
    this.name = 'AppError';
    this.code = code;
  }
}

class UserError extends AppError {
  constructor(message, userId) {
    super(message, 'USER_ERROR');
    this.userId = userId;
  }
}

class SystemError extends AppError {
  constructor(message, component) {
    super(message, 'SYSTEM_ERROR');
    this.component = component;
  }
}

function handleError(error) {
  if (error instanceof UserError) {
    console.log(`User error: ${error.message} (User: ${error.userId})`);
  } else if (error instanceof SystemError) {
    console.log(`System error: ${error.message} (Component: ${error.component})`);
  } else if (error instanceof AppError) {
    console.log(`App error: ${error.message} (Code: ${error.code})`);
  } else {
    console.log(`Unknown error: ${error.message}`);
  }
}

try {
  throw new UserError('Invalid user input', 123);
} catch (error) {
  handleError(error);
}

Error Handling Patterns

// Pattern 1: Early error detection
function processData(data) {
  if (!data) {
    throw new Error('Data is required');
  }
  if (!Array.isArray(data)) {
    throw new TypeError('Data must be an array');
  }
  if (data.length === 0) {
    throw new Error('Data array cannot be empty');
  }
  
  return data.map(item => item * 2);
}

// Pattern 2: Error wrapping
function safeOperation(operation) {
  try {
    return operation();
  } catch (error) {
    throw new Error(`Operation failed: ${error.message}`);
  }
}

// Pattern 3: Error aggregation
function validateObject(obj, schema) {
  const errors = [];
  
  for (const [field, rules] of Object.entries(schema)) {
    const value = obj[field];
    
    if (rules.required && (value === undefined || value === null)) {
      errors.push(`${field} is required`);
    }
    
    if (rules.type && typeof value !== rules.type) {
      errors.push(`${field} must be of type ${rules.type}`);
    }
    
    if (rules.min && value < rules.min) {
      errors.push(`${field} must be at least ${rules.min}`);
    }
    
    if (rules.max && value > rules.max) {
      errors.push(`${field} must be at most ${rules.max}`);
    }
  }
  
  if (errors.length > 0) {
    throw new Error(`Validation failed: ${errors.join(', ')}`);
  }
  
  return true;
}

// Pattern 4: Retry mechanism
async function retryOperation(operation, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      console.log(`Attempt ${attempt} failed: ${error.message}`);
      
      if (attempt === maxRetries) {
        throw new Error(`Operation failed after ${maxRetries} attempts: ${error.message}`);
      }
      
      // Wait before retry
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}

// Pattern 5: Fallback values
function safeParseJSON(jsonString, fallback = null) {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    console.warn('JSON parsing failed:', error.message);
    return fallback;
  }
}

// Pattern 6: Error logging
function logError(error, context = {}) {
  const errorInfo = {
    message: error.message,
    name: error.name,
    stack: error.stack,
    timestamp: new Date().toISOString(),
    context
  };
  
  console.error('Error logged:', errorInfo);
  
  // In production, send to logging service
  // loggingService.logError(errorInfo);
}

// Pattern 7: Error boundaries (React-like concept)
class ErrorHandler {
  constructor() {
    this.errors = [];
  }
  
  handle(error, context = {}) {
    this.errors.push({
      error,
      context,
      timestamp: new Date()
    });
    
    logError(error, context);
  }
  
  getErrors() {
    return this.errors;
  }
  
  clearErrors() {
    this.errors = [];
  }
}

const errorHandler = new ErrorHandler();

function riskyOperation(id) {
  if (id < 0) {
    throw new Error('ID cannot be negative');
  }
  return `Processed ${id}`;
}

try {
  riskyOperation(-1);
} catch (error) {
  errorHandler.handle(error, { operation: 'riskyOperation', id: -1 });
}

Async Error Handling

Error handling with asynchronous operations requires special consideration due to the non-blocking nature of JavaScript.

Try-Catch with Promises

// Try-catch with async/await
async function fetchData(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch error:', error.message);
    throw error; // Re-throw for caller to handle
  }
}

// Using the async function
async function processData() {
  try {
    const data = await fetchData('https://api.example.com/data');
    console.log('Data received:', data);
    return data;
  } catch (error) {
    console.error('Failed to process data:', error.message);
    return null; // Return null on error
  }
}

// Promise-based error handling
function fetchWithPromise(url) {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
    .catch(error => {
      console.error('Promise error:', error.message);
      throw error;
    });
}

// Using promise-based function
fetchWithPromise('https://api.example.com/data')
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Final error handler:', error.message));

// Multiple async operations
async function processMultipleData() {
  const results = [];
  
  try {
    const data1 = await fetchData('https://api.example.com/data1');
    results.push(data1);
    
    const data2 = await fetchData('https://api.example.com/data2');
    results.push(data2);
    
    const data3 = await fetchData('https://api.example.com/data3');
    results.push(data3);
    
    return results;
  } catch (error) {
    console.error('Error in processing multiple data:', error.message);
    return results; // Return partial results
  }
}

// Parallel async operations with error handling
async function processParallelData() {
  const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.example.com/data3'
  ];
  
  try {
    const promises = urls.map(url => fetchData(url));
    const results = await Promise.all(promises);
    return results;
  } catch (error) {
    console.error('Error in parallel processing:', error.message);
    
    // Fallback to sequential processing
    const results = [];
    for (const url of urls) {
      try {
        const data = await fetchData(url);
        results.push(data);
      } catch (singleError) {
        console.error(`Failed to fetch ${url}:`, singleError.message);
        results.push(null); // Add null for failed request
      }
    }
    return results;
  }
}

// Promise.allSettled for handling all results
async function processAllSettled() {
  const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/invalid', // This will fail
    'https://api.example.com/data3'
  ];
  
  const promises = urls.map(url => 
    fetchData(url).catch(error => ({ error: error.message }))
  );
  
  const results = await Promise.allSettled(promises);
  
  return results.map(result => {
    if (result.status === 'fulfilled') {
      return result.value;
    } else {
      return { error: result.reason.message };
    }
  });
}

// Async error handling with timeout
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error(`Request timeout after ${timeout}ms`);
    }
    throw error;
  }
}

Event-Based Error Handling

// Error handling with event emitters
class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
  }
  
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(listener => {
        try {
          listener(...args);
        } catch (error) {
          console.error(`Error in ${event} listener:`, error.message);
          this.emit('error', error);
        }
      });
    }
  }
}

const emitter = new EventEmitter();

emitter.on('data', (data) => {
  if (data === null) {
    throw new Error('Null data received');
  }
  console.log('Processing data:', data);
});

emitter.on('error', (error) => {
  console.error('Event emitter error:', error.message);
});

// Test error handling
emitter.emit('data', 'valid data'); // Works fine
emitter.emit('data', null); // Triggers error handling

// Error handling with streams (Node.js-like)
class StreamReader {
  constructor(data) {
    this.data = data;
    this.position = 0;
    this.listeners = {};
  }
  
  on(event, listener) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(listener);
  }
  
  read() {
    try {
      if (this.position >= this.data.length) {
        this.emit('end');
        return;
      }
      
      const chunk = this.data[this.position++];
      this.emit('data', chunk);
      
      // Simulate async reading
      setTimeout(() => this.read(), 10);
    } catch (error) {
      this.emit('error', error);
    }
  }
  
  emit(event, ...args) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(listener => {
        listener(...args);
      });
    }
  }
}

const reader = new StreamReader([1, 2, 3, null, 5]);

reader.on('data', (chunk) => {
  if (chunk === null) {
    throw new Error('Null chunk encountered');
  }
  console.log('Read chunk:', chunk);
});

reader.on('error', (error) => {
  console.error('Stream error:', error.message);
});

reader.on('end', () => {
  console.log('Stream ended');
});

reader.read();

// Error handling with callbacks
function fetchDataCallback(url, callback) {
  fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
    .then(data => callback(null, data))
    .catch(error => callback(error, null));
}

// Using callback-based function
fetchDataCallback('https://api.example.com/data', (error, data) => {
  if (error) {
    console.error('Callback error:', error.message);
    return;
  }
  console.log('Callback data:', data);
});

// Error handling with generators
function* dataGenerator() {
  try {
    yield fetch('https://api.example.com/data1');
    yield fetch('https://api.example.com/data2');
    yield fetch('https://api.example.com/data3');
  } catch (error) {
    console.error('Generator error:', error.message);
    throw error;
  }
}

// Using generator with error handling
async function processGenerator() {
  const generator = dataGenerator();
  
  try {
    for await (const response of generator) {
      const data = await response.json();
      console.log('Generator data:', data);
    }
  } catch (error) {
    console.error('Generator processing error:', error.message);
  }
}

Advanced Async Patterns

// Circuit breaker pattern
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.threshold = threshold;
    this.timeout = timeout;
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
  }
  
  async execute(operation) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}

// Using circuit breaker
const circuitBreaker = new CircuitBreaker();

async function protectedOperation() {
  return await circuitBreaker.execute(async () => {
    const response = await fetch('https://api.example.com/data');
    return response.json();
  });
}

// Retry with exponential backoff
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      
      if (attempt === maxRetries) {
        throw new Error(`Operation failed after ${maxRetries} attempts: ${error.message}`);
      }
      
      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// Error handling with async generators
async function* asyncDataGenerator(urls) {
  for (const url of urls) {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();
      yield data;
    } catch (error) {
      console.error(`Failed to fetch ${url}:`, error.message);
      yield { error: error.message, url };
    }
  }
}

// Using async generator with error handling
async function processAsyncGenerator() {
  const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/invalid',
    'https://api.example.com/data3'
  ];
  
  for await (const data of asyncDataGenerator(urls)) {
    if (data.error) {
      console.log('Error in generator:', data.error);
    } else {
      console.log('Data from generator:', data);
    }
  }
}

// Error handling with async iterators
class AsyncErrorCollector {
  constructor() {
    this.errors = [];
  }
  
  async collect(iterable) {
    for await (const item of iterable) {
      try {
        await this.processItem(item);
      } catch (error) {
        this.errors.push({ item, error: error.message });
      }
    }
    return this.errors;
  }
  
  async processItem(item) {
    // Simulate processing that might fail
    if (Math.random() < 0.3) {
      throw new Error(`Failed to process ${item}`);
    }
    console.log('Processed:', item);
  }
}

// Using async error collector
const collector = new AsyncErrorCollector();
const items = [1, 2, 3, 4, 5];
const errors = await collector.collect(items);
console.log('Collected errors:', errors);

Practical Applications

Error handling is crucial in real-world applications for maintaining stability and providing good user experience.

API Error Handling

// Comprehensive API error handling
class ApiClient {
  constructor(baseURL, timeout = 10000) {
    this.baseURL = baseURL;
    this.timeout = timeout;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    
    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers
        }
      });
      
      clearTimeout(timeoutId);
      
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new ApiError(
          `HTTP ${response.status}: ${response.statusText}`,
          response.status,
          errorData
        );
      }
      
      return await response.json();
    } catch (error) {
      clearTimeout(timeoutId);
      
      if (error.name === 'AbortError') {
        throw new ApiError('Request timeout', 408);
      }
      
      if (error instanceof ApiError) {
        throw error;
      }
      
      throw new ApiError(`Network error: ${error.message}`, 0);
    }
  }
  
  async get(endpoint) {
    return this.request(endpoint, { method: 'GET' });
  }
  
  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
  
  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }
  
  async delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }
}

class ApiError extends Error {
  constructor(message, statusCode, data = {}) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.data = data;
  }
}

// Using the API client
const api = new ApiClient('https://api.example.com');

async function fetchUser(userId) {
  try {
    const user = await api.get(`/users/${userId}`);
    return user;
  } catch (error) {
    if (error instanceof ApiError) {
      switch (error.statusCode) {
        case 404:
          console.log('User not found');
          return null;
        case 401:
          console.log('Authentication required');
          throw new Error('Please login to continue');
        case 403:
          console.log('Access denied');
          throw new Error('You do not have permission to access this resource');
        case 500:
          console.log('Server error, please try again later');
          throw new Error('Service temporarily unavailable');
        default:
          console.log('API error:', error.message);
          throw error;
      }
    } else {
      console.log('Network error:', error.message);
      throw new Error('Unable to connect to the server');
    }
  }
}

// API error handling with retry
async function fetchUserWithRetry(userId, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fetchUser(userId);
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }
      
      console.log(`Attempt ${attempt} failed, retrying...`);
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}

// Batch API operations with error handling
async function fetchMultipleUsers(userIds) {
  const results = new Map();
  
  await Promise.allSettled(
    userIds.map(async (userId) => {
      try {
        const user = await fetchUser(userId);
        results.set(userId, { status: 'success', data: user });
      } catch (error) {
        results.set(userId, { status: 'error', error: error.message });
      }
    })
  );
  
  return results;
}

// Usage examples
try {
  const user = await fetchUserWithRetry(123);
  console.log('User:', user);
} catch (error) {
  console.error('Final error:', error.message);
}

const userIds = [1, 2, 3, 999, 5];
const userResults = await fetchMultipleUsers(userIds);
console.log('User results:', userResults);

Form Validation Error Handling

// Comprehensive form validation
class FormValidator {
  constructor(rules) {
    this.rules = rules;
    this.errors = {};
  }
  
  validate(data) {
    this.errors = {};
    
    for (const [field, fieldRules] of Object.entries(this.rules)) {
      const value = data[field];
      const fieldErrors = this.validateField(field, value, fieldRules);
      
      if (fieldErrors.length > 0) {
        this.errors[field] = fieldErrors;
      }
    }
    
    return {
      isValid: Object.keys(this.errors).length === 0,
      errors: this.errors
    };
  }
  
  validateField(field, value, rules) {
    const errors = [];
    
    for (const rule of rules) {
      const error = this.applyRule(field, value, rule);
      if (error) {
        errors.push(error);
      }
    }
    
    return errors;
  }
  
  applyRule(field, value, rule) {
    const { type, message, ...params } = rule;
    
    switch (type) {
      case 'required':
        if (!value || value === '') {
          return message || `${field} is required`;
        }
        break;
        
      case 'email':
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (value && !emailRegex.test(value)) {
          return message || `${field} must be a valid email`;
        }
        break;
        
      case 'minLength':
        if (value && value.length < params.min) {
          return message || `${field} must be at least ${params.min} characters`;
        }
        break;
        
      case 'maxLength':
        if (value && value.length > params.max) {
          return message || `${field} must be at most ${params.max} characters`;
        }
        break;
        
      case 'pattern':
        if (value && !params.regex.test(value)) {
          return message || `${field} format is invalid`;
        }
        break;
        
      case 'custom':
        if (params.validator && !params.validator(value)) {
          return message || `${field} is invalid`;
        }
        break;
    }
    
    return null;
  }
}

// Validation rules
const userFormRules = {
  name: [
    { type: 'required', message: 'Name is required' },
    { type: 'minLength', min: 2, message: 'Name must be at least 2 characters' },
    { type: 'maxLength', max: 50, message: 'Name must be at most 50 characters' }
  ],
  email: [
    { type: 'required', message: 'Email is required' },
    { type: 'email', message: 'Please enter a valid email address' }
  ],
  age: [
    { type: 'required', message: 'Age is required' },
    { type: 'custom', validator: (value) => {
      const age = parseInt(value);
      return !isNaN(age) && age >= 18 && age <= 120;
    }, message: 'Age must be between 18 and 120' }
  ],
  password: [
    { type: 'required', message: 'Password is required' },
    { type: 'minLength', min: 8, message: 'Password must be at least 8 characters' },
    { type: 'pattern', regex: /[A-Z]/, message: 'Password must contain at least one uppercase letter' },
    { type: 'pattern', regex: /[0-9]/, message: 'Password must contain at least one number' }
  ]
};

// Using the validator
function validateUserForm(formData) {
  const validator = new FormValidator(userFormRules);
  const result = validator.validate(formData);
  
  if (!result.isValid) {
    console.log('Form validation errors:');
    for (const [field, errors] of Object.entries(result.errors)) {
      console.log(`${field}:`, errors);
    }
    throw new ValidationError('Form validation failed', result.errors);
  }
  
  return formData;
}

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

// Form submission with error handling
async function submitUserForm(formData) {
  try {
    // Validate form data
    const validatedData = validateUserForm(formData);
    
    // Submit to API
    const response = await api.post('/users', validatedData);
    
    return response;
  } catch (error) {
    if (error instanceof ValidationError) {
      console.log('Validation errors:', error.errors);
      return { success: false, errors: error.errors };
    } else if (error instanceof ApiError) {
      console.log('API error:', error.message);
      return { success: false, error: 'Server error occurred' };
    } else {
      console.log('Unexpected error:', error.message);
      return { success: false, error: 'An unexpected error occurred' };
    }
  }
}

// Real-time validation
class RealTimeValidator {
  constructor(rules) {
    this.validator = new FormValidator(rules);
    this.fieldValidators = {};
  }
  
  validateField(field, value) {
    const fieldRules = this.rules[field];
    if (!fieldRules) return { isValid: true, errors: [] };
    
    const errors = this.validator.validateField(field, value, fieldRules);
    return {
      isValid: errors.length === 0,
      errors
    };
  }
  
  setupFieldValidation(formElement) {
    const fields = formElement.querySelectorAll('input, select, textarea');
    
    fields.forEach(field => {
      const fieldName = field.name;
      if (!fieldName) return;
      
      field.addEventListener('blur', () => {
        const value = field.value;
        const result = this.validateField(fieldName, value);
        
        this.showFieldErrors(field, result);
      });
      
      field.addEventListener('input', () => {
        // Clear errors on input
        this.clearFieldErrors(field);
      });
    });
  }
  
  showFieldErrors(field, result) {
    this.clearFieldErrors(field);
    
    if (!result.isValid) {
      const errorElement = document.createElement('div');
      errorElement.className = 'field-error';
      errorElement.textContent = result.errors[0]; // Show first error
      
      field.parentNode.appendChild(errorElement);
      field.classList.add('error');
    }
  }
  
  clearFieldErrors(field) {
    const errorElement = field.parentNode.querySelector('.field-error');
    if (errorElement) {
      errorElement.remove();
    }
    field.classList.remove('error');
  }
}

Database Error Handling

// Database error handling patterns
class DatabaseError extends Error {
  constructor(message, query, params, originalError) {
    super(message);
    this.name = 'DatabaseError';
    this.query = query;
    this.params = params;
    this.originalError = originalError;
  }
}

class DatabaseConnection {
  constructor(config) {
    this.config = config;
    this.connection = null;
  }
  
  async connect() {
    try {
      // Simulate database connection
      if (this.config.invalid) {
        throw new Error('Invalid database configuration');
      }
      
      this.connection = { connected: true };
      console.log('Database connected successfully');
    } catch (error) {
      throw new DatabaseError(
        'Failed to connect to database',
        'CONNECT',
        this.config,
        error
      );
    }
  }
  
  async query(sql, params = []) {
    if (!this.connection) {
      throw new DatabaseError('Database not connected', sql, params);
    }
    
    try {
      // Simulate query execution
      if (sql.includes('ERROR')) {
        throw new Error('Query syntax error');
      }
      
      console.log('Executing query:', sql, params);
      return { rows: [], affectedRows: 0 };
    } catch (error) {
      throw new DatabaseError(
        'Query execution failed',
        sql,
        params,
        error
      );
    }
  }
  
  async transaction(queries) {
    if (!this.connection) {
      throw new DatabaseError('Database not connected', 'TRANSACTION', queries);
    }
    
    try {
      console.log('Starting transaction');
      
      for (const { sql, params } of queries) {
        await this.query(sql, params);
      }
      
      console.log('Transaction completed successfully');
    } catch (error) {
      console.log('Transaction failed, rolling back');
      throw new DatabaseError(
        'Transaction failed',
        'TRANSACTION',
        queries,
        error
      );
    }
  }
  
  async close() {
    try {
      if (this.connection) {
        this.connection = null;
        console.log('Database connection closed');
      }
    } catch (error) {
      throw new DatabaseError(
        'Failed to close database connection',
        'CLOSE',
        {},
        error
      );
    }
  }
}

// Repository pattern with error handling
class UserRepository {
  constructor(db) {
    this.db = db;
  }
  
  async create(userData) {
    try {
      const sql = 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)';
      const params = [userData.name, userData.email, userData.age];
      
      const result = await this.db.query(sql, params);
      return { id: result.insertId, ...userData };
    } catch (error) {
      if (error instanceof DatabaseError) {
        if (error.originalError.message.includes('UNIQUE constraint')) {
          throw new Error('User with this email already exists');
        }
        if (error.originalError.message.includes('NOT NULL')) {
          throw new Error('Required field is missing');
        }
      }
      throw error;
    }
  }
  
  async findById(id) {
    try {
      const sql = 'SELECT * FROM users WHERE id = ?';
      const params = [id];
      
      const result = await this.db.query(sql, params);
      return result.rows[0] || null;
    } catch (error) {
      if (error instanceof DatabaseError) {
        console.error('Database error in findById:', error.message);
        throw new Error('Failed to fetch user');
      }
      throw error;
    }
  }
  
  async update(id, userData) {
    try {
      const sql = 'UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?';
      const params = [userData.name, userData.email, userData.age, id];
      
      const result = await this.db.query(sql, params);
      return result.affectedRows > 0;
    } catch (error) {
      if (error instanceof DatabaseError) {
        console.error('Database error in update:', error.message);
        throw new Error('Failed to update user');
      }
      throw error;
    }
  }
  
  async delete(id) {
    try {
      const sql = 'DELETE FROM users WHERE id = ?';
      const params = [id];
      
      const result = await this.db.query(sql, params);
      return result.affectedRows > 0;
    } catch (error) {
      if (error instanceof DatabaseError) {
        console.error('Database error in delete:', error.message);
        throw new Error('Failed to delete user');
      }
      throw error;
    }
  }
}

// Service layer with comprehensive error handling
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }
  
  async createUser(userData) {
    try {
      // Validate input
      this.validateUserData(userData);
      
      // Create user
      const user = await this.userRepository.create(userData);
      
      // Log success
      console.log('User created successfully:', user.id);
      
      return user;
    } catch (error) {
      console.error('Failed to create user:', error.message);
      
      // Categorize and re-throw with appropriate message
      if (error.message.includes('already exists')) {
        throw new Error('A user with this email already exists');
      }
      
      if (error.message.includes('Required field')) {
        throw new Error('Please provide all required fields');
      }
      
      if (error.message.includes('Failed to fetch')) {
        throw new Error('Database error occurred');
      }
      
      throw new Error('Failed to create user');
    }
  }
  
  async getUser(id) {
    try {
      if (!id || id <= 0) {
        throw new Error('Invalid user ID');
      }
      
      const user = await this.userRepository.findById(id);
      
      if (!user) {
        throw new Error('User not found');
      }
      
      return user;
    } catch (error) {
      console.error('Failed to get user:', error.message);
      
      if (error.message.includes('Invalid user ID')) {
        throw error;
      }
      
      if (error.message.includes('User not found')) {
        throw error;
      }
      
      throw new Error('Failed to fetch user');
    }
  }
  
  validateUserData(userData) {
    if (!userData.name || userData.name.trim().length < 2) {
      throw new Error('Name must be at least 2 characters');
    }
    
    if (!userData.email || !this.isValidEmail(userData.email)) {
      throw new Error('Valid email is required');
    }
    
    if (!userData.age || userData.age < 18 || userData.age > 120) {
      throw new Error('Age must be between 18 and 120');
    }
  }
  
  isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

// Using the service layer
async function demonstrateUserService() {
  const db = new DatabaseConnection({ invalid: false });
  await db.connect();
  
  try {
    const userRepository = new UserRepository(db);
    const userService = new UserService(userRepository);
    
    // Create user
    const newUser = await userService.createUser({
      name: 'John Doe',
      email: 'john@example.com',
      age: 30
    });
    
    console.log('Created user:', newUser);
    
    // Get user
    const user = await userService.getUser(newUser.id);
    console.log('Fetched user:', user);
    
  } catch (error) {
    console.error('Service error:', error.message);
  } finally {
    await db.close();
  }
}

Best Practices

Following best practices ensures robust, maintainable, and user-friendly error handling in JavaScript applications.

Error Handling Principles

// Principle 1: Fail fast and fail loudly
function processCriticalData(data) {
  if (!data) {
    throw new Error('Data is required for processing');
  }
  
  if (!Array.isArray(data)) {
    throw new TypeError('Data must be an array');
  }
  
  if (data.length === 0) {
    throw new Error('Data array cannot be empty');
  }
  
  // Continue processing
  return data.map(item => item * 2);
}

// Principle 2: Handle errors at the appropriate level
function lowLevelOperation() {
  // This function should not handle errors, just throw them
  if (Math.random() < 0.3) {
    throw new Error('Low level operation failed');
  }
  return 'success';
}

function midLevelOperation() {
  try {
    return lowLevelOperation();
  } catch (error) {
    // Handle or transform the error
    console.error('Mid level error:', error.message);
    throw new Error('Mid level operation failed');
  }
}

function highLevelOperation() {
  try {
    return midLevelOperation();
  } catch (error) {
    // Handle user-facing errors
    console.error('High level error:', error.message);
    return { success: false, error: error.message };
  }
}

// Principle 3: Provide meaningful error messages
class UserError extends Error {
  constructor(message, userMessage) {
    super(message);
    this.name = 'UserError';
    this.userMessage = userMessage || message;
  }
}

function validateUserInput(input) {
  if (!input) {
    throw new UserError(
      'Input is null or undefined',
      'Please provide a value'
    );
  }
  
  if (typeof input !== 'string') {
    throw new UserError(
      `Expected string, got ${typeof input}`,
      'Please provide a valid text value'
    );
  }
  
  if (input.trim().length === 0) {
    throw new UserError(
      'Input is empty after trimming',
      'Please provide a non-empty value'
    );
  }
  
  return input.trim();
}

// Principle 4: Log errors appropriately
class ErrorLogger {
  constructor() {
    this.errors = [];
  }
  
  log(error, context = {}) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      message: error.message,
      name: error.name,
      stack: error.stack,
      context
    };
    
    this.errors.push(logEntry);
    
    // In production, send to logging service
    if (process.env.NODE_ENV === 'production') {
      this.sendToLoggingService(logEntry);
    } else {
      console.error('Logged error:', logEntry);
    }
  }
  
  sendToLoggingService(logEntry) {
    // Implementation for external logging service
    console.log('Sending to logging service:', logEntry);
  }
  
  getErrors() {
    return this.errors;
  }
}

const errorLogger = new ErrorLogger();

// Principle 5: Don't ignore errors
function badExample() {
  try {
    riskyOperation();
  } catch (error) {
    // Error is silently ignored
  }
}

function goodExample() {
  try {
    riskyOperation();
  } catch (error) {
    errorLogger.log(error, { operation: 'riskyOperation' });
    // Handle the error appropriately
    throw error;
  }
}

// Principle 6: Use specific error types
function processPayment(amount) {
  if (amount <= 0) {
    throw new ValidationError('Amount must be positive');
  }
  
  if (amount > 10000) {
    throw new BusinessRuleError('Amount exceeds maximum limit');
  }
  
  // Process payment
  return { success: true, amount };
}

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

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

// Principle 7: Provide fallback mechanisms
function safeParseJSON(jsonString, fallback = {}) {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    errorLogger.log(error, { jsonString });
    return fallback;
  }
}

function safeGetLocalStorage(key, fallback = null) {
  try {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : fallback;
  } catch (error) {
    errorLogger.log(error, { key });
    return fallback;
  }
}

Code Organization

// Organize error handling in modules
// errors.js
export class AppError extends Error {
  constructor(message, code, details = {}) {
    super(message);
    this.name = 'AppError';
    this.code = code;
    this.details = details;
    this.timestamp = new Date();
  }
}

export class ValidationError extends AppError {
  constructor(message, field, value) {
    super(message, 'VALIDATION_ERROR', { field, value });
    this.field = field;
    this.value = value;
  }
}

export class NetworkError extends AppError {
  constructor(message, statusCode, url) {
    super(message, 'NETWORK_ERROR', { statusCode, url });
    this.statusCode = statusCode;
    this.url = url;
  }
}

export class DatabaseError extends AppError {
  constructor(message, query, params) {
    super(message, 'DATABASE_ERROR', { query, params });
    this.query = query;
    this.params = params;
  }
}

// errorHandler.js
import { AppError } from './errors.js';

export class ErrorHandler {
  constructor(logger) {
    this.logger = logger;
  }
  
  handle(error, context = {}) {
    this.logError(error, context);
    return this.createErrorResponse(error);
  }
  
  logError(error, context) {
    const logData = {
      message: error.message,
      name: error.name,
      code: error.code,
      stack: error.stack,
      timestamp: new Date().toISOString(),
      context
    };
    
    this.logger.error('Application error', logData);
  }
  
  createErrorResponse(error) {
    if (error instanceof ValidationError) {
      return {
        success: false,
        error: 'Validation error',
        message: error.message,
        field: error.field
      };
    }
    
    if (error instanceof NetworkError) {
      return {
        success: false,
        error: 'Network error',
        message: 'Unable to connect to the server'
      };
    }
    
    if (error instanceof DatabaseError) {
      return {
        success: false,
        error: 'Database error',
        message: 'Data processing failed'
      };
    }
    
    // Generic error
    return {
      success: false,
      error: 'Application error',
      message: 'An unexpected error occurred'
    };
  }
}

// service.js
import { ErrorHandler } from './errorHandler.js';

class UserService {
  constructor(database, logger) {
    this.database = database;
    this.errorHandler = new ErrorHandler(logger);
  }
  
  async createUser(userData) {
    try {
      this.validateUserData(userData);
      const user = await this.database.createUser(userData);
      return { success: true, data: user };
    } catch (error) {
      return this.errorHandler.handle(error, { operation: 'createUser', userData });
    }
  }
  
  validateUserData(userData) {
    if (!userData.name || userData.name.trim().length < 2) {
      throw new ValidationError('Name must be at least 2 characters', 'name', userData.name);
    }
    
    if (!userData.email || !this.isValidEmail(userData.email)) {
      throw new ValidationError('Valid email is required', 'email', userData.email);
    }
  }
  
  isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

// Error boundary pattern (React-like)
class ErrorBoundary {
  constructor() {
    this.errors = [];
    this.onError = null;
  }
  
  catch(fn) {
    this.onError = fn;
    return this;
  }
  
  execute(operation) {
    try {
      return operation();
    } catch (error) {
      this.errors.push(error);
      if (this.onError) {
        this.onError(error);
      }
      return null;
    }
  }
  
  executeAsync(operation) {
    return operation()
      .then(result => result)
      .catch(error => {
        this.errors.push(error);
        if (this.onError) {
          this.onError(error);
        }
        return null;
      });
  }
  
  getErrors() {
    return this.errors;
  }
  
  clearErrors() {
    this.errors = [];
  }
}

// Using error boundary
const errorBoundary = new ErrorBoundary()
  .catch((error) => console.error('Caught error:', error.message));

const result1 = errorBoundary.execute(() => {
  return 2 + 2;
});

const result2 = errorBoundary.execute(() => {
  throw new Error('Something went wrong');
});

console.log(result1); // 4
console.log(result2); // null

Testing Error Handling

// Testing error scenarios
class ErrorTester {
  constructor() {
    this.testResults = [];
  }
  
  test(description, testFn) {
    try {
      const result = testFn();
      this.testResults.push({
        description,
        status: 'passed',
        result
      });
      console.log(`✓ ${description}`);
    } catch (error) {
      this.testResults.push({
        description,
        status: 'failed',
        error: error.message
      });
      console.log(`✗ ${description}: ${error.message}`);
    }
  }
  
  testAsync(description, testFn) {
    return testFn()
      .then(result => {
        this.testResults.push({
          description,
          status: 'passed',
          result
        });
        console.log(`✓ ${description}`);
      })
      .catch(error => {
        this.testResults.push({
          description,
          status: 'failed',
          error: error.message
        });
        console.log(`✗ ${description}: ${error.message}`);
      });
  }
  
  expectError(description, testFn, expectedErrorType) {
    try {
      testFn();
      this.testResults.push({
        description,
        status: 'failed',
        error: 'Expected error but none was thrown'
      });
      console.log(`✗ ${description}: Expected error but none was thrown`);
    } catch (error) {
      const errorMatches = !expectedErrorType || error instanceof expectedErrorType;
      if (errorMatches) {
        this.testResults.push({
          description,
          status: 'passed',
          result: error.message
        });
        console.log(`✓ ${description}: ${error.message}`);
      } else {
        this.testResults.push({
          description,
          status: 'failed',
          error: `Expected ${expectedErrorType.name}, got ${error.constructor.name}`
        });
        console.log(`✗ ${description}: Expected ${expectedErrorType.name}, got ${error.constructor.name}`);
      }
    }
  }
  
  getResults() {
    return this.testResults;
  }
  
  getSummary() {
    const passed = this.testResults.filter(r => r.status === 'passed').length;
    const failed = this.testResults.filter(r => r.status === 'failed').length;
    const total = this.testResults.length;
    
    return {
      total,
      passed,
      failed,
      passRate: ((passed / total) * 100).toFixed(2) + '%'
    };
  }
}

// Testing error handling functions
function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

function parseJSON(jsonString) {
  const result = JSON.parse(jsonString);
  return result;
}

function validateEmail(email) {
  if (!email) {
    throw new Error('Email is required');
  }
  if (!email.includes('@')) {
    throw new Error('Invalid email format');
  }
  return email;
}

// Running tests
const tester = new ErrorTester();

// Test successful cases
tester.test('Divide 10 by 2', () => divide(10, 2));
tester.test('Parse valid JSON', () => parseJSON('{"name": "John"}'));
tester.test('Validate valid email', () => validateEmail('test@example.com'));

// Test error cases
tester.expectError('Divide by zero', () => divide(10, 0), Error);
tester.expectError('Parse invalid JSON', () => parseJSON('invalid json'), SyntaxError);
tester.expectError('Validate empty email', () => validateEmail(''), Error);
tester.expectError('Validate invalid email', () => validateEmail('invalid'), Error);

// Test async error handling
async function fetchData(url) {
  if (url === 'error') {
    throw new Error('Network error');
  }
  return { data: 'success' };
}

tester.testAsync('Fetch valid data', () => fetchData('valid'));
tester.testAsync('Fetch with error', () => fetchData('error'));

// Test custom error types
class CustomError extends Error {
  constructor(message) {
    super(message);
    this.name = 'CustomError';
  }
}

function throwCustomError() {
  throw new CustomError('Custom error message');
}

tester.expectError('Throw custom error', throwCustomError, CustomError);

// Display results
console.log('\nTest Summary:');
console.log(tester.getSummary());
console.log('\nDetailed Results:');
tester.getResults().forEach(result => {
  console.log(`${result.status.toUpperCase()}: ${result.description}`);
  if (result.error) {
    console.log(`  Error: ${result.error}`);
  }
});

// Performance testing for error handling
function performanceTest() {
  const iterations = 100000;
  
  console.time('Try-catch performance');
  for (let i = 0; i < iterations; i++) {
    try {
      const result = i / 2;
    } catch (error) {
      // Handle error
    }
  }
  console.timeEnd('Try-catch performance');
  
  console.time('No try-catch performance');
  for (let i = 0; i < iterations; i++) {
    const result = i / 2;
  }
  console.timeEnd('No try-catch performance');
}

performanceTest();

Summary

Key Takeaways

  • Try-catch is JavaScript's fundamental error handling mechanism
  • Finally blocks always execute, regardless of errors
  • JavaScript has several built-in error types (Error, TypeError, ReferenceError, etc.)
  • Custom error classes provide better error categorization
  • Async error handling requires special consideration with promises and async/await
  • Error handling should be implemented at appropriate abstraction levels
  • Meaningful error messages improve debugging and user experience
  • Error logging and monitoring are essential for production applications

Best Practices

  • Fail fast and fail loud with clear error messages
  • Handle errors at the appropriate level of abstraction
  • Create custom error classes for better error categorization
  • Log errors with sufficient context for debugging
  • Provide fallback mechanisms for non-critical operations
  • Use try-catch-finally for resource cleanup
  • Implement retry mechanisms for transient failures
  • Test error handling scenarios thoroughly
  • Don't ignore errors - always handle or re-throw them
  • Use specific error types for different failure scenarios
  • Implement error boundaries for better error isolation
  • Provide user-friendly error messages while logging technical details

Common Pitfalls

  • Silently catching errors without proper handling
  • Using generic Error type instead of specific error types
  • Not providing enough context in error messages
  • Forgetting to include finally blocks for cleanup
  • Not handling errors in async operations properly
  • Catching errors at the wrong level of abstraction
  • Not logging errors for debugging purposes
  • Exposing internal error details to end users
  • Not implementing retry mechanisms for transient failures
  • Ignoring error handling in production code
  • Using try-catch for flow control instead of error conditions
  • Not testing error handling scenarios
  • Catching too broadly and masking important errors