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
- All errors must be logged - Never silently swallow errors
- Provide context - Include relevant metadata for debugging
- User-friendly messages - Show clear, actionable error messages to users
- Preserve error chains - Don't lose original error information
- 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_metadatatable 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.tsitself - 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)
- API Calls - All Supabase operations are tracked automatically via the wrapped client
- Navigation - Route changes are tracked automatically
- 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
- Visit
/test-error-logging(dev only) - Click "Generate Test Error"
- Check Admin Panel at
/admin/error-monitoring - 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.
Related Files
Core Error Handling:
src/lib/errorHandler.ts- Main error handling utilitiessrc/lib/errorBreadcrumbs.ts- Breadcrumb tracking systemsrc/lib/environmentContext.ts- Environment data capturesrc/lib/logger.ts- Structured logging utilitysrc/lib/supabaseClient.ts- Wrapped client with auto-tracking
Admin Tools:
src/pages/admin/ErrorMonitoring.tsx- Error dashboardsrc/pages/admin/ErrorLookup.tsx- Error ID searchsrc/components/admin/ErrorDetailsModal.tsx- Error details view
Edge Functions:
supabase/functions/_shared/logger.ts- Edge function logger
Database:
request_metadatatable - Stores all error logsrequest_breadcrumbstable - Stores breadcrumb trailslog_request_metadataRPC - Logs errors from client
Summary
Golden Rules:
- ✅ Use
handleError()for user-facing application errors - ✅ Use
logger.*for development debugging and expected failures - ✅ Use
toast.*for success/info notifications - ✅ Use
edgeLogger.*in edge functions - ❌ NEVER use
console.*directly in application code - ✅ Always preserve error context when catching
- ✅ Re-throw errors after handling for error boundaries
- ✅ Include descriptive action names and metadata
- ✅ Manual breadcrumbs for critical user actions only
- ✅ 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 });