Front-end Engineering Lab
Patterns

Advanced Error Handling

Production-grade error handling strategies for resilient frontend applications

Advanced Error Handling

Error handling is not just about catching exceptions—it's about building resilient systems that gracefully handle failures, provide great user experience, and give developers actionable insights.

Why Advanced Error Handling?

Basic try-catch is not enough for production applications:

  • User Errors vs Bugs: Different handling for expected vs unexpected errors
  • Recovery Strategies: Retry, fallback, or degrade gracefully
  • Context Preservation: Know what the user was doing when error occurred
  • Cascading Failures: One error shouldn't break the entire app
  • Actionable Reporting: Errors should help you fix issues quickly

Error Classification

1. User Errors (Expected)

Errors caused by user input or actions:

  • Form validation failures
  • Invalid search queries
  • Network timeouts
  • Authorization failures

Handling: Show friendly messages, guide user to fix

2. Application Errors (Bugs)

Unexpected errors in your code:

  • Null reference errors
  • Type errors
  • Logic errors
  • Integration failures

Handling: Log to monitoring, show fallback UI, alert team

3. System Errors (Infrastructure)

Platform or infrastructure issues:

  • API downtime
  • CDN failures
  • Browser incompatibilities
  • Resource exhaustion

Handling: Graceful degradation, retry with backoff, circuit breaker

Core Principles

1. Fail Gracefully

// ❌ BAD: Let errors crash the app
function ProductList() {
  const products = fetchProducts(); // Throws on failure
  return <div>{products.map(...)}</div>;
}

// ✅ GOOD: Handle errors gracefully
function ProductList() {
  const { data, error, isLoading } = useProducts();
  
  if (isLoading) return <Skeleton />;
  if (error) return <ErrorFallback error={error} retry={refetch} />;
  if (!data) return <EmptyState />;
  
  return <div>{data.map(...)}</div>;
}

2. Isolate Failures

// ❌ BAD: One error breaks everything
function Dashboard() {
  return (
    <>
      <UserProfile />
      <Notifications />
      <ActivityFeed />
    </>
  );
}

// ✅ GOOD: Isolate with error boundaries
function Dashboard() {
  return (
    <>
      <ErrorBoundary fallback={<ProfileError />}>
        <UserProfile />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<NotificationsError />}>
        <Notifications />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<FeedError />}>
        <ActivityFeed />
      </ErrorBoundary>
    </>
  );
}

3. Provide Context

// ❌ BAD: Generic error with no context
throw new Error('Failed to load');

// ✅ GOOD: Rich error context
class LoadError extends Error {
  constructor(
    message: string,
    public context: {
      userId: string;
      resource: string;
      attemptNumber: number;
      timestamp: Date;
    }
  ) {
    super(message);
    this.name = 'LoadError';
  }
}

throw new LoadError('Failed to load user profile', {
  userId: user.id,
  resource: '/api/profile',
  attemptNumber: 3,
  timestamp: new Date(),
});

4. Enable Recovery

// Provide ways for users to recover
function ErrorFallback({ error, resetError }: ErrorFallbackProps) {
  return (
    <div className="error-container">
      <h2>Something went wrong</h2>
      <p>{error.message}</p>
      
      <div className="actions">
        <button onClick={resetError}>Try Again</button>
        <button onClick={() => window.location.reload()}>
          Reload Page
        </button>
        <button onClick={() => router.push('/')}>
          Go Home
        </button>
      </div>
    </div>
  );
}

Error Handling Strategy

// lib/error-handler.ts
export class ErrorHandler {
  static handle(error: Error, context?: any) {
    // 1. Classify error
    const errorType = this.classify(error);
    
    // 2. Log with context
    this.log(error, errorType, context);
    
    // 3. Report if needed
    if (errorType === 'bug' || errorType === 'system') {
      this.report(error, context);
    }
    
    // 4. Determine recovery strategy
    return this.getRecoveryStrategy(errorType);
  }
  
  private static classify(error: Error): 'user' | 'bug' | 'system' {
    // User errors
    if (error instanceof ValidationError) return 'user';
    if (error instanceof NotFoundError) return 'user';
    
    // System errors
    if (error instanceof NetworkError) return 'system';
    if (error instanceof TimeoutError) return 'system';
    
    // Everything else is a bug
    return 'bug';
  }
  
  private static getRecoveryStrategy(type: string) {
    const strategies = {
      user: { retry: false, notify: true, fallback: false },
      bug: { retry: false, notify: true, fallback: true },
      system: { retry: true, notify: true, fallback: true },
    };
    
    return strategies[type];
  }
}

Error Types Hierarchy

// lib/errors.ts

// Base application error
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500,
    public isOperational: boolean = true
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

// User-facing errors (operational)
export class ValidationError extends AppError {
  constructor(message: string, public field?: string) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 'NOT_FOUND', 404);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 'UNAUTHORIZED', 401);
  }
}

// System errors (operational)
export class NetworkError extends AppError {
  constructor(message: string, public originalError?: Error) {
    super(message, 'NETWORK_ERROR', 503);
  }
}

export class TimeoutError extends AppError {
  constructor(operation: string, timeout: number) {
    super(`${operation} timed out after ${timeout}ms`, 'TIMEOUT', 408);
  }
}

// Programming errors (non-operational)
export class ConfigurationError extends AppError {
  constructor(message: string) {
    super(message, 'CONFIG_ERROR', 500, false);
  }
}

Monitoring Integration

// lib/error-reporter.ts
import * as Sentry from '@sentry/nextjs';

export function reportError(
  error: Error,
  context?: {
    user?: any;
    tags?: Record<string, string>;
    level?: 'error' | 'warning' | 'info';
  }
) {
  // Don't report user errors to monitoring
  if (error instanceof ValidationError) return;
  if (error instanceof NotFoundError) return;
  
  // Add context
  Sentry.setContext('error_context', context);
  
  // Set severity
  Sentry.captureException(error, {
    level: context?.level || 'error',
    tags: context?.tags,
  });
  
  // Log locally for debugging
  if (process.env.NODE_ENV === 'development') {
    console.error('Error reported:', error, context);
  }
}

Best Practices

  1. Separate User Errors from Bugs: Handle them differently
  2. Add Context: Include user ID, action, timestamp
  3. Fail Gracefully: Never show blank screens
  4. Isolate Failures: Use error boundaries to contain errors
  5. Enable Recovery: Provide retry/reset options
  6. Monitor Proactively: Alert on error rate spikes
  7. Test Error States: Include error scenarios in tests

Common Pitfalls

Catching all errors silently: Users see broken UI
Show appropriate error UI

Reporting every error to monitoring: Alert fatigue
Report only bugs and system errors

Generic error messages: "Something went wrong"
Specific, actionable messages

No error recovery: Users have to refresh
Provide retry/reset mechanisms

Implementation Checklist

  • Define error type hierarchy
  • Set up error boundaries at key levels
  • Implement error classification logic
  • Configure error monitoring (Sentry, etc.)
  • Add context to all error reports
  • Implement retry strategies
  • Design fallback UIs
  • Add circuit breakers for external services
  • Test error scenarios
  • Document error handling patterns

Next Steps

Explore specific error handling patterns:

On this page