Error Handling and Recovery 🛡️
Build resilient reptile care software with comprehensive error handling. A guide to graceful failure management in CrittrHavens.

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. 🐍