Files
thrilltrack-explorer/docs/ERROR_HANDLING_GUIDE.md
2025-11-04 17:34:16 +00:00

15 KiB

Error Handling Guide

This guide outlines the standardized error handling patterns used throughout ThrillWiki to ensure consistent, debuggable, and user-friendly error management.

Core Principles

  1. All errors must be logged - Never silently swallow errors
  2. Provide context - Include relevant metadata for debugging
  3. User-friendly messages - Show clear, actionable error messages to users
  4. Preserve error chains - Don't lose original error information
  5. Use structured logging - Avoid raw console.* statements

When to Use What

handleError() - Application Errors (User-Facing)

Use handleError() for errors that affect user operations and should be visible in the Admin Panel.

When to use:

  • Database operation failures
  • API call failures
  • Form submission errors
  • Authentication/authorization failures
  • Any error that impacts user workflows

Example:

import { handleError } from '@/lib/errorHandler';
import { useAuth } from '@/hooks/useAuth';

try {
  await supabase.from('parks').insert(parkData);
  handleSuccess('Park Created', 'Your park has been added successfully');
} catch (error) {
  handleError(error, {
    action: 'Create Park',
    userId: user?.id,
    metadata: { parkName: parkData.name }
  });
  throw error; // Re-throw for parent error boundaries
}

Key features:

  • Logs to request_metadata table with full context
  • Shows user-friendly toast with error reference ID
  • Captures breadcrumbs (last 10 user actions)
  • Visible in Admin Panel at /admin/error-monitoring

logger.* - Development & Debugging Logs

Use logger.* for information that helps developers debug issues without sending data to the database.

When to use:

  • Development debugging information
  • Performance monitoring
  • Expected failures that don't need Admin Panel visibility
  • Component lifecycle events
  • Non-critical informational messages

Available methods:

import { logger } from '@/lib/logger';

// Development only - not logged in production
logger.log('Component mounted', { props });
logger.info('User action completed', { action: 'click' });
logger.warn('Deprecated API used', { api: 'oldMethod' });
logger.debug('State updated', { newState });

// Always logged - even in production
logger.error('Critical failure', { context });

// Specialized logging
logger.performance('ComponentName', durationMs);
logger.moderationAction('approve', itemId, durationMs);

Example - Expected periodic failures:

// Don't show toast or log to Admin Panel for expected periodic failures
try {
  await supabase.rpc('release_expired_locks');
} catch (error) {
  logger.debug('Periodic lock release failed', { 
    operation: 'release_expired_locks',
    error: getErrorMessage(error)
  });
}

toast.* - User Notifications

Use toast notifications directly for informational messages, warnings, or confirmations.

When to use:

  • Success confirmations (use handleSuccess() helper)
  • Informational messages
  • Non-error warnings
  • User confirmations

Example:

import { handleSuccess, handleInfo } from '@/lib/errorHandler';

// Success messages
handleSuccess('Changes Saved', 'Your profile has been updated');

// Informational messages
handleInfo('Processing', 'Your request is being processed');

// Custom toast for special cases
toast.info('Feature Coming Soon', {
  description: 'This feature will be available next month',
  duration: 4000
});

console.* - NEVER USE DIRECTLY

DO NOT USE console.* statements in application code. They are blocked by ESLint.

// ❌ WRONG - Will fail ESLint check
console.log('User clicked button');
console.error('Database error:', error);

// ✅ CORRECT - Use logger or handleError
logger.log('User clicked button');
handleError(error, { action: 'Database Operation', userId });

The only exceptions:

  • Inside src/lib/logger.ts itself
  • Edge function logging (use edgeLogger.*)
  • Test files (*.test.ts, *.test.tsx)

Error Handling Patterns

Pattern 1: Component/Hook Errors (Most Common)

For errors in components or custom hooks that affect user operations:

import { handleError } from '@/lib/errorHandler';
import { useAuth } from '@/hooks/useAuth';

const MyComponent = () => {
  const { user } = useAuth();

  const handleSubmit = async (data: FormData) => {
    try {
      await saveData(data);
      handleSuccess('Saved', 'Your changes have been saved');
    } catch (error) {
      handleError(error, {
        action: 'Save Form Data',
        userId: user?.id,
        metadata: { formType: 'parkEdit' }
      });
      throw error; // Re-throw for error boundaries
    }
  };
};

Key points:

  • Always include descriptive action name
  • Include userId when available
  • Add relevant metadata for debugging
  • Re-throw after handling to let error boundaries catch it

Pattern 2: TanStack Query Errors

For errors within React Query hooks:

import { useQuery } from '@tanstack/react-query';
import { handleError } from '@/lib/errorHandler';

const { data, error, isLoading } = useQuery({
  queryKey: ['parks', parkId],
  queryFn: async () => {
    const { data, error } = await supabase
      .from('parks')
      .select('*')
      .eq('id', parkId)
      .single();
    
    if (error) {
      handleError(error, {
        action: 'Fetch Park Details',
        userId: user?.id,
        metadata: { parkId }
      });
      throw error;
    }
    
    return data;
  }
});

// Handle error state in UI
if (error) {
  return <ErrorState message="Failed to load park" />;
}

Pattern 3: Expected/Recoverable Errors

For operations that may fail expectedly and should be logged but not shown to users:

import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errorHandler';

// Background operation that may fail without impacting user
const syncCache = async () => {
  try {
    await performCacheSync();
  } catch (error) {
    // Log for debugging without user notification
    logger.warn('Cache sync failed', { 
      operation: 'syncCache',
      error: getErrorMessage(error)
    });
    // Continue execution - cache sync is non-critical
  }
};

Pattern 4: Error Boundaries (Top-Level)

React Error Boundaries catch unhandled component errors:

import { Component, ReactNode } from 'react';
import { handleError } from '@/lib/errorHandler';

class ErrorBoundary extends Component<
  { children: ReactNode },
  { hasError: boolean }
> {
  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    handleError(error, {
      action: 'Component Error Boundary',
      metadata: { 
        componentStack: errorInfo.componentStack 
      }
    });
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }
    return this.props.children;
  }
}

Pattern 5: Preserve Error Context in Chains

When catching and re-throwing errors, preserve the original error information:

// ❌ WRONG - Loses original error
try {
  await operation();
} catch (error) {
  throw new Error('Operation failed'); // Original error lost!
}

// ❌ WRONG - Silent catch loses context
const data = await fetch(url)
  .then(res => res.json())
  .catch(() => ({ message: 'Failed' })); // Error details lost!

// ✅ CORRECT - Preserve and log error
try {
  const response = await fetch(url);
  if (!response.ok) {
    const errorData = await response.json().catch((parseError) => {
      logger.warn('Failed to parse error response', { 
        error: getErrorMessage(parseError),
        status: response.status
      });
      return { message: 'Request failed' };
    });
    throw new Error(errorData.message);
  }
  return await response.json();
} catch (error) {
  handleError(error, {
    action: 'Fetch Data',
    userId: user?.id,
    metadata: { url }
  });
  throw error;
}

Automatic Breadcrumb Tracking

The application automatically tracks breadcrumbs (last 10 user actions) to provide context for errors.

Automatic Tracking (No Code Needed)

  1. API Calls - All Supabase operations are tracked automatically via the wrapped client
  2. Navigation - Route changes are tracked automatically
  3. Mutation Errors - TanStack Query mutations log failures automatically

Manual Breadcrumb Tracking

Add breadcrumbs for important user actions:

import { breadcrumb } from '@/lib/errorBreadcrumbs';

// Navigation breadcrumb (usually automatic)
breadcrumb.navigation('/parks/123', '/parks');

// User action breadcrumb
breadcrumb.userAction('clicked submit', 'ParkEditForm', { 
  parkId: '123' 
});

// API call breadcrumb (usually automatic via wrapped client)
breadcrumb.apiCall('/api/parks', 'POST', 200);

// State change breadcrumb
breadcrumb.stateChange('filter changed', { 
  filter: 'status=open' 
});

When to add manual breadcrumbs:

  • Critical user actions (form submissions, deletions)
  • Important state changes (filter updates, mode switches)
  • Non-Supabase API calls
  • Complex user workflows

When NOT to add breadcrumbs:

  • Inside loops or frequently called functions
  • For every render or effect
  • For trivial state changes
  • Inside already tracked operations

Edge Function Error Handling

Edge functions use a separate logger to prevent sensitive data exposure:

import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';

Deno.serve(async (req) => {
  const tracking = startRequest();
  
  try {
    // Your edge function logic
    const result = await performOperation();
    
    const duration = endRequest(tracking);
    edgeLogger.info('Operation completed', {
      requestId: tracking.requestId,
      duration
    });
    
    return new Response(JSON.stringify(result), {
      headers: { 'Content-Type': 'application/json' }
    });
  } catch (error) {
    const duration = endRequest(tracking);
    
    edgeLogger.error('Operation failed', {
      requestId: tracking.requestId,
      error: error.message,
      duration
    });
    
    return new Response(
      JSON.stringify({ 
        error: 'Operation failed',
        requestId: tracking.requestId 
      }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    );
  }
});

Key features:

  • Automatic sanitization of sensitive fields
  • Request correlation IDs
  • Structured JSON logging
  • Duration tracking

Testing Error Handling

Manual Testing

  1. Visit /test-error-logging (dev only)
  2. Click "Generate Test Error"
  3. Check Admin Panel at /admin/error-monitoring
  4. Verify error appears with:
    • Full stack trace
    • Breadcrumbs (including API calls)
    • Environment context
    • User information

Automated Testing

import { handleError } from '@/lib/errorHandler';

describe('Error Handling', () => {
  it('should log errors to database', async () => {
    const mockError = new Error('Test error');
    
    handleError(mockError, {
      action: 'Test Action',
      metadata: { test: true }
    });
    
    // Verify error logged to request_metadata table
    const { data } = await supabase
      .from('request_metadata')
      .select('*')
      .eq('error_message', 'Test error')
      .single();
    
    expect(data).toBeDefined();
    expect(data.endpoint).toBe('Test Action');
  });
});

Common Mistakes to Avoid

Mistake 1: Silent Error Catching

// ❌ WRONG
try {
  await operation();
} catch (error) {
  // Nothing - error disappears!
}

// ✅ CORRECT
try {
  await operation();
} catch (error) {
  logger.debug('Expected operation failure', { 
    operation: 'name',
    error: getErrorMessage(error)
  });
}

Mistake 2: Using console.* Directly

// ❌ WRONG - Blocked by ESLint
console.log('Debug info', data);
console.error('Error occurred', error);

// ✅ CORRECT
logger.log('Debug info', data);
handleError(error, { action: 'Operation Name', userId });

Mistake 3: Not Re-throwing After Handling

// ❌ WRONG - Error doesn't reach error boundary
try {
  await operation();
} catch (error) {
  handleError(error, { action: 'Operation' });
  // Error stops here - error boundary never sees it
}

// ✅ CORRECT
try {
  await operation();
} catch (error) {
  handleError(error, { action: 'Operation' });
  throw error; // Let error boundary handle UI fallback
}

Mistake 4: Generic Error Messages

// ❌ WRONG - No context
handleError(error, { action: 'Error' });

// ✅ CORRECT - Descriptive context
handleError(error, {
  action: 'Update Park Opening Hours',
  userId: user?.id,
  metadata: { 
    parkId: park.id,
    parkName: park.name
  }
});

Mistake 5: Losing Error Context

// ❌ WRONG
.catch(() => ({ error: 'Failed' }))

// ✅ CORRECT
.catch((error) => {
  logger.warn('Operation failed', { error: getErrorMessage(error) });
  return { error: 'Failed' };
})

Error Monitoring Dashboard

Access the error monitoring dashboard at /admin/error-monitoring:

Features:

  • Real-time error list with filtering
  • Search by error ID, message, or user
  • Full stack traces
  • Breadcrumb trails showing user actions before error
  • Environment context (browser, device, network)
  • Request metadata (endpoint, method, status)

Error ID Lookup: Visit /admin/error-lookup to search for specific errors by their 8-character reference ID shown to users.

Core Error Handling:

  • src/lib/errorHandler.ts - Main error handling utilities
  • src/lib/errorBreadcrumbs.ts - Breadcrumb tracking system
  • src/lib/environmentContext.ts - Environment data capture
  • src/lib/logger.ts - Structured logging utility
  • src/lib/supabaseClient.ts - Wrapped client with auto-tracking

Admin Tools:

  • src/pages/admin/ErrorMonitoring.tsx - Error dashboard
  • src/pages/admin/ErrorLookup.tsx - Error ID search
  • src/components/admin/ErrorDetailsModal.tsx - Error details view

Edge Functions:

  • supabase/functions/_shared/logger.ts - Edge function logger

Database:

  • request_metadata table - Stores all error logs
  • request_breadcrumbs table - Stores breadcrumb trails
  • log_request_metadata RPC - Logs errors from client

Summary

Golden Rules:

  1. Use handleError() for user-facing application errors
  2. Use logger.* for development debugging and expected failures
  3. Use toast.* for success/info notifications
  4. Use edgeLogger.* in edge functions
  5. NEVER use console.* directly in application code
  6. Always preserve error context when catching
  7. Re-throw errors after handling for error boundaries
  8. Include descriptive action names and metadata
  9. Manual breadcrumbs for critical user actions only
  10. Test error handling in Admin Panel

Quick Reference:

// Application error (user-facing)
handleError(error, { action: 'Action Name', userId, metadata });

// Debug log (development only)
logger.debug('Debug info', { context });

// Expected failure (log but don't show toast)
logger.warn('Expected failure', { error: getErrorMessage(error) });

// Success notification
handleSuccess('Title', 'Description');

// Edge function error
edgeLogger.error('Error message', { requestId, error: error.message });