- Add extended error types with severity levels, retry policies, and contextual information (types/errors.ts) - Create centralized error handler service with user-friendly message translation and classification (services/errorHandler.ts) - Add ErrorContext for global error state management with auto-dismiss and error queue support (contexts/ErrorContext.tsx) - Create error UI components: ErrorToast, FieldError, FieldErrorSummary, FullScreenError, EmptyState, OfflineState - Add useError hook with retry strategies and API response handling - Add useAsync hook for async operations with comprehensive state - Create error message utilities with validation helpers - Add tests for errorHandler and errorMessages (88 tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
369 lines
9.3 KiB
TypeScript
369 lines
9.3 KiB
TypeScript
/**
|
|
* useError - Hook for error handling with recovery strategies
|
|
*
|
|
* Features:
|
|
* - Handle API errors consistently
|
|
* - Automatic retry with exponential backoff
|
|
* - User-friendly error display
|
|
* - Error state management
|
|
*/
|
|
|
|
import { useState, useCallback, useRef } from 'react';
|
|
import { Alert } from 'react-native';
|
|
import {
|
|
AppError,
|
|
ErrorState,
|
|
initialErrorState,
|
|
isRetryableError,
|
|
} from '@/types/errors';
|
|
import {
|
|
createErrorFromApi,
|
|
createErrorFromUnknown,
|
|
calculateRetryDelay,
|
|
logError,
|
|
} from '@/services/errorHandler';
|
|
import { ApiError, ApiResponse } from '@/types';
|
|
import { useShowErrorSafe } from '@/contexts/ErrorContext';
|
|
|
|
interface UseErrorOptions {
|
|
// Show error in global toast
|
|
showToast?: boolean;
|
|
// Show native alert
|
|
showAlert?: boolean;
|
|
// Custom alert title
|
|
alertTitle?: string;
|
|
// Auto-retry on failure
|
|
autoRetry?: boolean;
|
|
// Max retry attempts (overrides error default)
|
|
maxRetries?: number;
|
|
// Callback when error occurs
|
|
onError?: (error: AppError) => void;
|
|
// Callback when retry starts
|
|
onRetryStart?: (attempt: number) => void;
|
|
// Callback when all retries exhausted
|
|
onRetryExhausted?: (error: AppError) => void;
|
|
}
|
|
|
|
interface UseErrorReturn {
|
|
// Current error state
|
|
errorState: ErrorState;
|
|
|
|
// Set error from API response
|
|
setApiError: (error: ApiError, context?: Record<string, unknown>) => void;
|
|
|
|
// Set error from any value
|
|
setError: (error: unknown, context?: Record<string, unknown>) => void;
|
|
|
|
// Clear current error
|
|
clearError: () => void;
|
|
|
|
// Clear all errors
|
|
clearAllErrors: () => void;
|
|
|
|
// Handle API response (returns data if ok, sets error if not)
|
|
handleApiResponse: <T>(
|
|
response: ApiResponse<T>,
|
|
context?: Record<string, unknown>
|
|
) => T | null;
|
|
|
|
// Wrap async function with error handling
|
|
wrapAsync: <T>(
|
|
fn: () => Promise<T>,
|
|
context?: Record<string, unknown>
|
|
) => Promise<T | null>;
|
|
|
|
// Execute with retry
|
|
executeWithRetry: <T>(
|
|
fn: () => Promise<ApiResponse<T>>,
|
|
context?: Record<string, unknown>
|
|
) => Promise<T | null>;
|
|
|
|
// Check if currently has error
|
|
hasError: boolean;
|
|
|
|
// Current error (if any)
|
|
error: AppError | null;
|
|
}
|
|
|
|
export function useError(options: UseErrorOptions = {}): UseErrorReturn {
|
|
const {
|
|
showToast = true,
|
|
showAlert = false,
|
|
alertTitle = 'Error',
|
|
autoRetry = false,
|
|
maxRetries,
|
|
onError,
|
|
onRetryStart,
|
|
onRetryExhausted,
|
|
} = options;
|
|
|
|
const [errorState, setErrorState] = useState<ErrorState>(initialErrorState);
|
|
const retryCountRef = useRef<Map<string, number>>(new Map());
|
|
|
|
// Get global show error function (may not be available if not in ErrorProvider)
|
|
const showGlobalError = useShowErrorSafe();
|
|
|
|
// Internal function to handle error
|
|
const handleError = useCallback(
|
|
(appError: AppError, retryFn?: () => void) => {
|
|
// Log error
|
|
logError(appError);
|
|
|
|
// Update state
|
|
setErrorState({
|
|
hasError: true,
|
|
error: appError,
|
|
errors: [appError],
|
|
lastErrorAt: new Date(),
|
|
});
|
|
|
|
// Call callback
|
|
onError?.(appError);
|
|
|
|
// Show toast
|
|
if (showToast && showGlobalError) {
|
|
showGlobalError(appError, { onRetry: retryFn });
|
|
}
|
|
|
|
// Show alert
|
|
if (showAlert) {
|
|
const buttons: { text: string; onPress?: () => void; style?: 'cancel' | 'default' | 'destructive' }[] = [
|
|
{ text: 'OK', style: 'cancel' as const },
|
|
];
|
|
|
|
if (isRetryableError(appError) && retryFn) {
|
|
buttons.unshift({
|
|
text: 'Retry',
|
|
onPress: retryFn,
|
|
});
|
|
}
|
|
|
|
Alert.alert(alertTitle, appError.userMessage, buttons);
|
|
}
|
|
},
|
|
[showToast, showAlert, alertTitle, onError, showGlobalError]
|
|
);
|
|
|
|
// Set error from API error
|
|
const setApiError = useCallback(
|
|
(error: ApiError, context?: Record<string, unknown>) => {
|
|
const appError = createErrorFromApi(error, context);
|
|
handleError(appError);
|
|
},
|
|
[handleError]
|
|
);
|
|
|
|
// Set error from any value
|
|
const setError = useCallback(
|
|
(error: unknown, context?: Record<string, unknown>) => {
|
|
const appError = createErrorFromUnknown(error, undefined, context);
|
|
handleError(appError);
|
|
},
|
|
[handleError]
|
|
);
|
|
|
|
// Clear current error
|
|
const clearError = useCallback(() => {
|
|
setErrorState(initialErrorState);
|
|
}, []);
|
|
|
|
// Clear all errors
|
|
const clearAllErrors = useCallback(() => {
|
|
setErrorState(initialErrorState);
|
|
retryCountRef.current.clear();
|
|
}, []);
|
|
|
|
// Handle API response
|
|
const handleApiResponse = useCallback(
|
|
<T>(response: ApiResponse<T>, context?: Record<string, unknown>): T | null => {
|
|
if (response.ok && response.data !== undefined) {
|
|
clearError();
|
|
return response.data;
|
|
}
|
|
|
|
if (response.error) {
|
|
setApiError(response.error, context);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
[clearError, setApiError]
|
|
);
|
|
|
|
// Wrap async function with error handling
|
|
const wrapAsync = useCallback(
|
|
async <T>(
|
|
fn: () => Promise<T>,
|
|
context?: Record<string, unknown>
|
|
): Promise<T | null> => {
|
|
try {
|
|
const result = await fn();
|
|
clearError();
|
|
return result;
|
|
} catch (error) {
|
|
setError(error, context);
|
|
return null;
|
|
}
|
|
},
|
|
[clearError, setError]
|
|
);
|
|
|
|
// Execute with retry
|
|
const executeWithRetry = useCallback(
|
|
async <T>(
|
|
fn: () => Promise<ApiResponse<T>>,
|
|
context?: Record<string, unknown>
|
|
): Promise<T | null> => {
|
|
const operationId = Date.now().toString();
|
|
let attempt = 0;
|
|
const maxAttempts = maxRetries ?? 3;
|
|
|
|
const execute = async (): Promise<T | null> => {
|
|
try {
|
|
const response = await fn();
|
|
|
|
if (response.ok && response.data !== undefined) {
|
|
clearError();
|
|
retryCountRef.current.delete(operationId);
|
|
return response.data;
|
|
}
|
|
|
|
if (response.error) {
|
|
const appError = createErrorFromApi(response.error, context);
|
|
|
|
// Check if should retry
|
|
const shouldRetry =
|
|
autoRetry &&
|
|
isRetryableError(appError) &&
|
|
attempt < maxAttempts;
|
|
|
|
if (shouldRetry) {
|
|
attempt++;
|
|
onRetryStart?.(attempt);
|
|
|
|
const delay = calculateRetryDelay(appError.retry, attempt - 1);
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
|
|
return execute();
|
|
}
|
|
|
|
// No more retries
|
|
if (attempt > 0) {
|
|
onRetryExhausted?.(appError);
|
|
}
|
|
|
|
handleError(appError, autoRetry ? () => execute() : undefined);
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
const appError = createErrorFromUnknown(error, undefined, context);
|
|
|
|
// Check if should retry
|
|
const shouldRetry =
|
|
autoRetry &&
|
|
isRetryableError(appError) &&
|
|
attempt < maxAttempts;
|
|
|
|
if (shouldRetry) {
|
|
attempt++;
|
|
onRetryStart?.(attempt);
|
|
|
|
const delay = calculateRetryDelay(appError.retry, attempt - 1);
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
|
|
return execute();
|
|
}
|
|
|
|
// No more retries
|
|
if (attempt > 0) {
|
|
onRetryExhausted?.(appError);
|
|
}
|
|
|
|
handleError(appError, autoRetry ? () => execute() : undefined);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return execute();
|
|
},
|
|
[autoRetry, maxRetries, clearError, handleError, onRetryStart, onRetryExhausted]
|
|
);
|
|
|
|
return {
|
|
errorState,
|
|
setApiError,
|
|
setError,
|
|
clearError,
|
|
clearAllErrors,
|
|
handleApiResponse,
|
|
wrapAsync,
|
|
executeWithRetry,
|
|
hasError: errorState.hasError,
|
|
error: errorState.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* useFieldErrors - Hook for managing field-level validation errors
|
|
*/
|
|
interface FieldErrors {
|
|
[field: string]: string | undefined;
|
|
}
|
|
|
|
interface UseFieldErrorsReturn {
|
|
errors: FieldErrors;
|
|
setFieldError: (field: string, message: string) => void;
|
|
clearFieldError: (field: string) => void;
|
|
clearAllFieldErrors: () => void;
|
|
hasErrors: boolean;
|
|
getError: (field: string) => string | undefined;
|
|
getErrorList: () => { field: string; message: string }[];
|
|
setErrors: (errors: FieldErrors) => void;
|
|
}
|
|
|
|
export function useFieldErrors(): UseFieldErrorsReturn {
|
|
const [errors, setErrors] = useState<FieldErrors>({});
|
|
|
|
const setFieldError = useCallback((field: string, message: string) => {
|
|
setErrors((prev) => ({ ...prev, [field]: message }));
|
|
}, []);
|
|
|
|
const clearFieldError = useCallback((field: string) => {
|
|
setErrors((prev) => {
|
|
const { [field]: _, ...rest } = prev;
|
|
return rest;
|
|
});
|
|
}, []);
|
|
|
|
const clearAllFieldErrors = useCallback(() => {
|
|
setErrors({});
|
|
}, []);
|
|
|
|
const getError = useCallback(
|
|
(field: string) => errors[field],
|
|
[errors]
|
|
);
|
|
|
|
const getErrorList = useCallback(() => {
|
|
return Object.entries(errors)
|
|
.filter(([_, message]) => message !== undefined)
|
|
.map(([field, message]) => ({ field, message: message as string }));
|
|
}, [errors]);
|
|
|
|
const hasErrors = Object.values(errors).some((v) => v !== undefined);
|
|
|
|
return {
|
|
errors,
|
|
setFieldError,
|
|
clearFieldError,
|
|
clearAllFieldErrors,
|
|
hasErrors,
|
|
getError,
|
|
getErrorList,
|
|
setErrors,
|
|
};
|
|
}
|
|
|
|
export default useError;
|