Advanced Error Handling

Custom errors, error boundaries, global handlers, and debugging strategies

Robust Error Handling

Proper error handling is crucial for building reliable applications. JavaScript provides mechanisms for catching, creating, and propagating errors. Understanding these patterns helps you build resilient code that fails gracefully.

Error Types

  • Error — Base error type
  • SyntaxError — Invalid code syntax
  • ReferenceError — Invalid variable reference
  • TypeError — Value is wrong type
  • RangeError — Value outside valid range

try/catch/finally

try {
  // Code that might throw
  const result = riskyOperation();
  console.log(result);
  
} catch (error) {
  // Handle the error
  console.error("Error:", error.message);
  
} finally {
  // Always runs (cleanup code)
  cleanup();
}

// Catch specific error types
try {
  JSON.parse("invalid json");
} catch (error) {
  if (error instanceof SyntaxError) {
    console.log("Invalid JSON");
  } else if (error instanceof TypeError) {
    console.log("Type error");
  } else {
    throw error; // Re-throw unknown errors
  }
}

// Optional catch binding (ES2019)
try {
  doSomething();
} catch {
  // 'error' parameter is optional if unused
  console.log("Something went wrong");
}

Custom Error Classes

// Base custom error
class AppError extends Error {
  constructor(message, code) {
    super(message);
    this.name = this.constructor.name;
    this.code = code;
    
    // Maintains proper stack trace (V8)
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
class ValidationError extends AppError {
  constructor(message, field) {
    super(message, "VALIDATION_ERROR");
    this.field = field;
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, "NOT_FOUND");
    this.resource = resource;
  }
}

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

// Usage
function validateUser(user) {
  if (!user.email) {
    throw new ValidationError("Email is required", "email");
  }
  if (!user.email.includes("@")) {
    throw new ValidationError("Invalid email format", "email");
  }
}

try {
  validateUser({ name: "Alice" });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Field: ${error.field}, Message: ${error.message}`);
  }
}

Async Error Handling

// Promises - use .catch()
fetchData()
  .then(data => process(data))
  .then(result => save(result))
  .catch(error => {
    console.error("Pipeline failed:", error);
  });

// async/await - use try/catch
async function loadUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    
    if (!response.ok) {
      throw new NetworkError("Failed to fetch user", response.status);
    }
    
    return await response.json();
    
  } catch (error) {
    if (error instanceof NetworkError) {
      // Handle network errors
      showNotification("Network error. Please try again.");
    } else {
      // Unexpected errors
      console.error("Unexpected error:", error);
      throw error;
    }
  }
}

// Wrapper for consistent error handling
async function tryCatch(promise) {
  try {
    const data = await promise;
    return [data, null];
  } catch (error) {
    return [null, error];
  }
}

// Usage
const [user, error] = await tryCatch(fetchUser(1));
if (error) {
  handleError(error);
} else {
  displayUser(user);
}

// Handle multiple async operations
const results = await Promise.allSettled([
  fetchUsers(),
  fetchPosts(),
  fetchComments()
]);

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

Global Error Handlers

// Browser - Uncaught errors
window.onerror = function(message, source, line, column, error) {
  console.log("Global error:", { message, source, line, column });
  logToServer({ message, source, line, column, stack: error?.stack });
  return true; // Prevents default browser error handling
};

// Better: Use addEventListener
window.addEventListener("error", (event) => {
  console.error("Uncaught error:", event.error);
  event.preventDefault();
});

// Unhandled Promise rejections
window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled promise rejection:", event.reason);
  event.preventDefault();
  
  // Log to error tracking service
  logToServer({
    type: "unhandledRejection",
    error: event.reason?.message || event.reason
  });
});

// Node.js equivalents
process.on("uncaughtException", (error) => {
  console.error("Uncaught Exception:", error);
  process.exit(1);
});

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection:", reason);
});

Error Boundaries (React Pattern)

// React Error Boundary
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    // Log to error reporting service
    logErrorToService(error, errorInfo.componentStack);
  }
  
  render() {
    if (this.state.hasError) {
      return this.props.fallback || 

Something went wrong.

; } return this.props.children; } } // Usage }>

Error Handling Patterns

// Result pattern (inspired by Rust)
class Result {
  constructor(value, error) {
    this.value = value;
    this.error = error;
  }
  
  static ok(value) {
    return new Result(value, null);
  }
  
  static err(error) {
    return new Result(null, error);
  }
  
  isOk() {
    return this.error === null;
  }
  
  isErr() {
    return this.error !== null;
  }
  
  map(fn) {
    return this.isOk() ? Result.ok(fn(this.value)) : this;
  }
  
  unwrap() {
    if (this.isErr()) throw this.error;
    return this.value;
  }
}

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.value); // 5
}

// Retry with backoff
async function withRetry(fn, maxRetries = 3, delay = 1000) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {

  
      lastError = error;
      
      if (attempt < maxRetries) {
        const waitTime = delay * Math.pow(2, attempt - 1);
        console.log(`Attempt ${attempt} failed, retrying in ${waitTime}ms`);
        await new Promise(r => setTimeout(r, waitTime));
      }
    }
  }
  
  throw lastError;
}

const data = await withRetry(() => fetch("/api/data"), 3, 1000);

💡 Key Takeaways

  • • Create custom error classes for domain-specific errors
  • • Use try/catch for sync, .catch() or try/catch for async
  • • Set up global handlers for uncaught errors and rejections
  • • Log errors to external services (Sentry, LogRocket, etc.)
  • • Use Error Boundaries in React for component-level recovery
  • • Consider Result/Either patterns for predictable error handling