Error Handling and Recovery 🛡️

Build resilient reptile care software with comprehensive error handling. A guide to graceful failure management in CrittrHavens.

Abstract warning sign with error alert concept on dark background

Error Handling Philosophy

Failures are opportunities for graceful recovery.

Core Principles

Error Management Strategy:

  • Fail Fast, Recover Gracefully: Detect errors early, handle them smoothly
  • User-First Communication: Clear, actionable error messages
  • Progressive Degradation: Maintain core functionality when possible
  • Comprehensive Logging: Track errors for debugging and improvement
  • Automatic Recovery: Self-healing where appropriate

Error Categories

Classification System:

// Error severity levels
enum ErrorSeverity {
  LOW = 'low',        // Minor UI glitches
  MEDIUM = 'medium',  // Feature degradation
  HIGH = 'high',      // Data loss risk
  CRITICAL = 'critical' // System failure
}

// Error types
enum ErrorType {
  NETWORK = 'network',
  VALIDATION = 'validation',
  PERMISSION = 'permission',
  DATA = 'data',
  SYSTEM = 'system',
  UNKNOWN = 'unknown'
}

Error Boundary Implementation

React's safety net for component failures.

Basic Error Boundary

Component Error Isolation:

import React, { Component, ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { logger } from '@/lib/logger';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
  level?: 'page' | 'section' | 'component';
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
  errorCount: number;
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = {
    hasError: false,
    error: null,
    errorCount: 0
  };

  static getDerivedStateFromError(error: Error): Partial<State> {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log error to monitoring service
    logger.error('Component error:', {
      error: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
      level: this.props.level || 'component'
    });

    // Call custom error handler
    this.props.onError?.(error, errorInfo);
  }

  handleReset = () => {
    this.setState({
      hasError: false,
      error: null,
      errorCount: this.state.errorCount + 1
    });
  };

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback;
      }

      return (
        <Card className="p-6 m-4">
          <h2 className="text-xl font-semibold mb-2">
            Something went wrong
          </h2>
          <p className="text-muted-foreground mb-4">
            {this.state.error?.message || 'An unexpected error occurred'}
          </p>
          {this.state.errorCount < 3 && (
            <Button onClick={this.handleReset}>
              Try Again
            </Button>
          )}
        </Card>
      );
    }

    return this.props.children;
  }
}

Specialized Error Boundaries

Feature-Specific Handlers:

// Form error boundary
export class FormErrorBoundary extends ErrorBoundary {
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    super.componentDidCatch(error, errorInfo);
    
    // Clear form data from cache
    localStorage.removeItem('form-draft');
    
    // Show form-specific recovery options
    toast.error('Form error - your data has been saved');
  }
}

// Data fetching boundary
export class DataErrorBoundary extends ErrorBoundary {
  handleRetry = async () => {
    // Invalidate cache and retry
    await queryClient.invalidateQueries();
    this.handleReset();
  };
}

Error Boundary Hierarchy

Nested Protection:

function App() {
  return (
    <ErrorBoundary level="page">
      <Router>
        <ErrorBoundary level="section">
          <Header />
        </ErrorBoundary>
        
        <ErrorBoundary level="section">
          <Routes>
            <Route path="/crittrs" element={
              <ErrorBoundary level="component">
                <CrittrList />
              </ErrorBoundary>
            } />
          </Routes>
        </ErrorBoundary>
      </Router>
    </ErrorBoundary>
  );
}

Error Types and Classification

Understanding different error scenarios.

Custom Error Classes

Typed Error System:

// Base error class
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public severity: ErrorSeverity,
    public recoverable: boolean = true
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// Specific error types
export class NetworkError extends AppError {
  constructor(message: string, public statusCode?: number) {
    super(
      message,
      'NETWORK_ERROR',
      ErrorSeverity.MEDIUM,
      true
    );
  }
}

export class ValidationError extends AppError {
  constructor(
    message: string,
    public fields: Record<string, string>
  ) {
    super(
      message,
      'VALIDATION_ERROR',
      ErrorSeverity.LOW,
      true
    );
  }
}

export class AuthenticationError extends AppError {
  constructor(message: string) {
    super(
      message,
      'AUTH_ERROR',
      ErrorSeverity.HIGH,
      false
    );
  }
}

Error Analysis

Intelligent Error Classification:

export function analyzeError(error: unknown): ErrorAnalysis {
  // Network errors
  if (error instanceof TypeError && error.message.includes('fetch')) {
    return {
      type: ErrorType.NETWORK,
      severity: ErrorSeverity.MEDIUM,
      recoverable: true,
      userMessage: 'Connection issue. Please check your internet.',
      suggestion: 'Retry in a few seconds'
    };
  }
  
  // Supabase errors
  if (error?.code === 'PGRST116') {
    return {
      type: ErrorType.PERMISSION,
      severity: ErrorSeverity.HIGH,
      recoverable: false,
      userMessage: 'Permission denied',
      suggestion: 'Please log in again'
    };
  }
  
  // Validation errors
  if (error?.code === 'VALIDATION_FAILED') {
    return {
      type: ErrorType.VALIDATION,
      severity: ErrorSeverity.LOW,
      recoverable: true,
      userMessage: 'Please check your input',
      suggestion: 'Fix the highlighted fields'
    };
  }
  
  // Unknown errors
  return {
    type: ErrorType.UNKNOWN,
    severity: ErrorSeverity.HIGH,
    recoverable: false,
    userMessage: 'An unexpected error occurred',
    suggestion: 'Please refresh and try again'
  };
}

User-Friendly Error Messages

Communicating errors effectively.

Message Guidelines

Clear Communication:

// ❌ Bad: Technical jargon
"Error: ECONNREFUSED 127.0.0.1:5432"

// ✅ Good: User-friendly
"Unable to connect to the database. Please try again."

// ❌ Bad: Vague
"Something went wrong"

// ✅ Good: Specific and actionable
"Failed to save your crittr. Check your internet connection and try again."

Error Message Components

Structured Error Display:

interface ErrorMessageProps {
  error: AppError;
  onRetry?: () => void;
  onDismiss?: () => void;
}

export function ErrorMessage({ error, onRetry, onDismiss }: ErrorMessageProps) {
  const analysis = analyzeError(error);
  
  return (
    <Alert variant={analysis.severity === 'critical' ? 'destructive' : 'warning'}>
      <AlertCircle className="h-4 w-4" />
      <AlertTitle>{analysis.userMessage}</AlertTitle>
      <AlertDescription>
        {analysis.suggestion}
        
        <div className="mt-4 flex gap-2">
          {analysis.recoverable && onRetry && (
            <Button size="sm" onClick={onRetry}>
              Try Again
            </Button>
          )}
          <Button size="sm" variant="outline" onClick={onDismiss}>
            Dismiss
          </Button>
        
      </AlertDescription>
    </Alert>
  );
}

Inline Error States

Form Field Errors:

export function FormFieldError({ field, error }: FormFieldErrorProps) {
  if (!error) return null;
  
  return (
    <div className="flex items-center gap-1 text-red-500 text-sm mt-1">
      <ExclamationCircle className="h-3 w-3" />
      <span role="alert">{error.message}</span>
    
  );
}

Error Logging and Reporting

Tracking errors for improvement.

Logging Strategy

Comprehensive Error Logging:

import * as Sentry from '@sentry/react';

class ErrorLogger {
  private isDevelopment = process.env.NODE_ENV === 'development';
  
  logError(error: Error, context?: Record<string, any>) {
    // Console logging in development
    if (this.isDevelopment) {
      console.error('Error:', error);
      console.table(context);
      return;
    }
    
    // Production logging
    Sentry.captureException(error, {
      extra: context,
      tags: {
        component: context?.component,
        severity: context?.severity
      }
    });
  }
  
  logWarning(message: string, data?: any) {
    if (this.isDevelopment) {
      console.warn(message, data);
    } else {
      Sentry.captureMessage(message, 'warning');
    }
  }
  
  trackErrorRate(errorType: string) {
    // Track error frequency for monitoring
    analytics.track('error_occurred', {
      type: errorType,
      timestamp: Date.now()
    });
  }
}

export const logger = new ErrorLogger();

Error Context Collection

Gathering Debug Information:

function collectErrorContext(): ErrorContext {
  return {
    // User context
    userId: getCurrentUser()?.id,
    subscription: getCurrentUser()?.subscription_status,
    
    // Session context
    sessionId: getSessionId(),
    timestamp: new Date().toISOString(),
    
    // Browser context
    userAgent: navigator.userAgent,
    url: window.location.href,
    viewport: {
      width: window.innerWidth,
      height: window.innerHeight
    },
    
    // App state
    route: getCurrentRoute(),
    isOnline: navigator.onLine,
    memoryUsage: performance.memory?.usedJSHeapSize
  };
}

Graceful Degradation Strategies

Maintaining functionality during failures.

Progressive Enhancement

Feature Degradation:

export function CrittrList() {
  const { data, error, isOffline } = useCrittrs();
  
  // Offline mode - show cached data
  if (isOffline) {
    return (
      <>
        <Alert>
          <WifiOff className="h-4 w-4" />
          <AlertDescription>
            Offline mode - showing cached data
          </AlertDescription>
        </Alert>
        <CachedCrittrList />
      </>
    );
  }
  
  // Partial failure - show what we have
  if (error && data?.partial) {
    return (
      <>
        <Alert variant="warning">
          Some data couldn't be loaded
        </Alert>
        <PartialCrittrList data={data.items} />
      </>
    );
  }
  
  // Complete failure - show fallback
  if (error && !data) {
    return <ErrorFallback error={error} />;
  }
  
  return <FullCrittrList data={data} />;
}

Fallback Components

Degraded Experiences:

// Read-only mode fallback
export function ReadOnlyFallback({ data }) {
  return (
    <div>
      <Alert>
        <InfoIcon className="h-4 w-4" />
        <AlertDescription>
          Viewing in read-only mode due to connection issues
        </AlertDescription>
      </Alert>
      <CrittrDisplay data={data} readonly />
    
  );
}

// Simplified UI fallback
export function SimplifiedUI({ data }) {
  return (
    <div className="space-y-2">
      {data.map(item => (
        <div key={item.id} className="p-2 border rounded">
          {item.name} - {item.species}
        
      ))}
    
  );
}

Retry Mechanisms

Automatic recovery from transient failures.

Exponential Backoff

Smart Retry Logic:

export class RetryMechanism {
  async executeWithRetry<T>(
    fn: () => Promise<T>,
    options: RetryOptions = {}
  ): Promise<RetryResult<T>> {
    const {
      maxAttempts = 3,
      baseDelay = 1000,
      maxDelay = 30000,
      backoffFactor = 2,
      jitter = true,
      retryCondition = this.defaultRetryCondition
    } = options;
    
    let lastError: Error;
    const startTime = Date.now();
    
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        const data = await fn();
        return {
          success: true,
          data,
          attempts: attempt,
          totalTime: Date.now() - startTime
        };
      } catch (error) {
        lastError = error as Error;
        
        // Check if we should retry
        if (!retryCondition(error, attempt)) {
          break;
        }
        
        // Don't retry on last attempt
        if (attempt === maxAttempts) {
          break;
        }
        
        // Calculate delay with exponential backoff
        let delay = Math.min(
          baseDelay * Math.pow(backoffFactor, attempt - 1),
          maxDelay
        );
        
        // Add jitter to prevent thundering herd
        if (jitter) {
          delay = delay * (0.5 + Math.random() * 0.5);
        }
        
        await this.delay(delay);
      }
    }
    
    return {
      success: false,
      error: lastError!,
      attempts: maxAttempts,
      totalTime: Date.now() - startTime
    };
  }
  
  private defaultRetryCondition(error: any, attempt: number): boolean {
    // Don't retry on client errors (4xx)
    if (error.status >= 400 && error.status < 500) {
      return false;
    }
    
    // Retry on network errors
    if (error.code === 'NETWORK_ERROR') {
      return true;
    }
    
    // Retry on server errors (5xx)
    if (error.status >= 500) {
      return attempt <= 3;
    }
    
    return false;
  }
  
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Circuit Breaker Pattern

Preventing Cascading Failures:

export class CircuitBreaker {
  private state: CircuitBreakerState = CircuitBreakerState.CLOSED;
  private failures = 0;
  private lastFailureTime?: number;
  private successCount = 0;
  
  constructor(
    private options: CircuitBreakerOptions = {
      failureThreshold: 5,
      recoveryTimeout: 60000,
      monitorTimeout: 120000
    }
  ) {}
  
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    // Check if circuit is open
    if (this.state === CircuitBreakerState.OPEN) {
      if (this.shouldAttemptReset()) {
        this.state = CircuitBreakerState.HALF_OPEN;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    
    if (this.state === CircuitBreakerState.HALF_OPEN) {
      this.successCount++;
      if (this.successCount >= 3) {
        this.state = CircuitBreakerState.CLOSED;
        this.successCount = 0;
      }
    }
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    
    if (this.failures >= this.options.failureThreshold) {
      this.state = CircuitBreakerState.OPEN;
    }
  }
  
  private shouldAttemptReset(): boolean {
    return Date.now() - this.lastFailureTime! >= this.options.recoveryTimeout;
  }
}

Offline Error Handling

Managing errors without connectivity.

Offline Detection

Network State Monitoring:

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  useEffect(() => {
    const handleOnline = () => {
      setIsOnline(true);
      toast.success('Back online!');
      // Sync queued operations
      syncQueue.processAll();
    };
    
    const handleOffline = () => {
      setIsOnline(false);
      toast.warning('You are offline. Changes will be saved locally.');
    };
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  return isOnline;
}

Offline Queue

Operation Queuing:

class OfflineQueue {
  private queue: QueuedOperation[] = [];
  
  add(operation: QueuedOperation) {
    this.queue.push(operation);
    this.persist();
  }
  
  async processAll() {
    const operations = [...this.queue];
    this.queue = [];
    
    for (const op of operations) {
      try {
        await this.processOperation(op);
      } catch (error) {
        // Re-queue failed operations
        this.queue.push(op);
      }
    }
    
    this.persist();
  }
  
  private async processOperation(op: QueuedOperation) {
    switch (op.type) {
      case 'CREATE':
        await supabase.from(op.table).insert(op.data);
        break;
      case 'UPDATE':
        await supabase.from(op.table).update(op.data).eq('id', op.id);
        break;
      case 'DELETE':
        await supabase.from(op.table).delete().eq('id', op.id);
        break;
    }
  }
  
  private persist() {
    localStorage.setItem('offline-queue', JSON.stringify(this.queue));
  }
}

Network Error Recovery

Handling connectivity issues gracefully.

Request Interceptors

Automatic Error Handling:

// Axios interceptor example
axios.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    // Retry on network timeout
    if (error.code === 'ECONNABORTED' && !originalRequest._retry) {
      originalRequest._retry = true;
      return axios(originalRequest);
    }
    
    // Handle auth expiration
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      await refreshAuthToken();
      return axios(originalRequest);
    }
    
    // Handle rate limiting
    if (error.response?.status === 429) {
      const retryAfter = error.response.headers['retry-after'];
      await delay(retryAfter * 1000);
      return axios(originalRequest);
    }
    
    return Promise.reject(error);
  }
);

Timeout Handling

Request Timeouts:

export async function fetchWithTimeout(
  url: string,
  options: RequestInit = {},
  timeout = 10000
): Promise<Response> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error.name === 'AbortError') {
      throw new Error(`Request timeout after ${timeout}ms`);
    }
    
    throw error;
  }
}

Form Validation Errors

Handling user input errors.

Field-Level Validation

Real-time Validation:

export function useFieldValidation(
  value: string,
  validators: Validator[]
) {
  const [error, setError] = useState<string | null>(null);
  const [isValidating, setIsValidating] = useState(false);
  
  const validate = useCallback(async () => {
    setIsValidating(true);
    
    for (const validator of validators) {
      const result = await validator(value);
      if (result !== true) {
        setError(result);
        setIsValidating(false);
        return false;
      }
    }
    
    setError(null);
    setIsValidating(false);
    return true;
  }, [value, validators]);
  
  // Debounced validation
  useEffect(() => {
    const timeoutId = setTimeout(validate, 500);
    return () => clearTimeout(timeoutId);
  }, [value, validate]);
  
  return { error, isValidating };
}

Form-Level Error Display

Error Summary:

export function FormErrors({ errors }: { errors: FieldErrors }) {
  const errorList = Object.entries(errors);
  
  if (errorList.length === 0) return null;
  
  return (
    <Alert variant="destructive" className="mb-4">
      <AlertCircle className="h-4 w-4" />
      <AlertTitle>Please fix the following errors:</AlertTitle>
      <AlertDescription>
        <ul className="list-disc pl-4 mt-2">
          {errorList.map(([field, error]) => (
            <li key={field}>
              <strong>{formatFieldName(field)}:</strong> {error.message}
            </li>
          ))}
        </ul>
      </AlertDescription>
    </Alert>
  );
}

Debugging Production Errors

Finding and fixing issues in production.

Error Breadcrumbs

Tracking User Actions:

class ErrorBreadcrumbs {
  private breadcrumbs: Breadcrumb[] = [];
  private maxBreadcrumbs = 50;
  
  addBreadcrumb(breadcrumb: Breadcrumb) {
    this.breadcrumbs.push({
      ...breadcrumb,
      timestamp: Date.now()
    });
    
    // Limit breadcrumb count
    if (this.breadcrumbs.length > this.maxBreadcrumbs) {
      this.breadcrumbs.shift();
    }
  }
  
  addNavigation(from: string, to: string) {
    this.addBreadcrumb({
      type: 'navigation',
      category: 'route',
      data: { from, to }
    });
  }
  
  addUserAction(action: string, target?: string) {
    this.addBreadcrumb({
      type: 'user',
      category: 'action',
      data: { action, target }
    });
  }
  
  addApiCall(method: string, url: string, status?: number) {
    this.addBreadcrumb({
      type: 'api',
      category: 'request',
      data: { method, url, status }
    });
  }
  
  getBreadcrumbs(): Breadcrumb[] {
    return [...this.breadcrumbs];
  }
}

Source Maps

Production Debugging:

// vite.config.ts
export default defineConfig({
  build: {
    sourcemap: true, // Enable source maps
  },
  // Upload source maps to error tracking service
  plugins: [
    sentryVitePlugin({
      authToken: process.env.SENTRY_AUTH_TOKEN,
      org: 'crittrhavens',
      project: 'frontend',
      include: './dist',
      ignore: ['node_modules'],
    })
  ]
});

Error Reproduction

Capturing Error State:

export function captureErrorState(error: Error): ErrorReport {
  return {
    error: {
      message: error.message,
      stack: error.stack,
      name: error.name
    },
    state: {
      route: window.location.pathname,
      timestamp: new Date().toISOString(),
      user: getCurrentUser(),
      breadcrumbs: breadcrumbs.getBreadcrumbs(),
      localStorage: captureLocalStorage(),
      sessionStorage: captureSessionStorage()
    },
    environment: {
      userAgent: navigator.userAgent,
      viewport: `${window.innerWidth}x${window.innerHeight}`,
      connection: navigator.connection?.effectiveType,
      memory: performance.memory
    }
  };
}

Best Practices

Error handling guidelines.

Do's and Don'ts

✅ DO:

  • Provide clear, actionable error messages
  • Log errors with sufficient context
  • Implement retry logic for transient failures
  • Use error boundaries to isolate failures
  • Test error scenarios thoroughly

❌ DON'T:

  • Swallow errors silently
  • Show technical details to users
  • Retry non-recoverable errors
  • Log sensitive information
  • Ignore error trends

Error Handling Checklist

Before deployment:

  • All async operations have error handling
  • Error boundaries protect critical paths
  • User-friendly messages for all error types
  • Retry logic for network requests
  • Offline mode handles errors gracefully
  • Logging captures sufficient context
  • Source maps configured for debugging
  • Error monitoring service integrated

Common Patterns

Reusable error handling solutions.

Global Error Handler

window.addEventListener('unhandledrejection', event => {
  logger.error('Unhandled promise rejection:', {
    reason: event.reason,
    promise: event.promise
  });
  
  // Prevent default browser error
  event.preventDefault();
  
  // Show user notification
  toast.error('An unexpected error occurred');
});

Error Recovery Hook

export function useErrorRecovery() {
  const [error, setError] = useState<Error | null>(null);
  const [isRecovering, setIsRecovering] = useState(false);
  
  const recover = useCallback(async () => {
    setIsRecovering(true);
    try {
      // Clear error state
      setError(null);
      
      // Reset application state
      await resetAppState();
      
      // Reload data
      await queryClient.refetchQueries();
      
      toast.success('Recovery successful');
    } catch (recoveryError) {
      toast.error('Recovery failed. Please refresh the page.');
    } finally {
      setIsRecovering(false);
    }
  }, []);
  
  return { error, setError, recover, isRecovering };
}

Next Steps

Continue building resilient applications.


Building reptile care software that handles errors as gracefully as a snake sheds its skin. 🐍