name: frontend/error-handling-standards
description: Implement error handling: try-catch at boundaries, typed errors with instanceof checks, error state in store, error boundaries for components, exponential backoff retry with jitter, Sentry monitoring in production, user-friendly error messages. Use when implementing API calls, form submissions, or component error boundaries.
Frontend Error Handling Standards
Pattern: Try-Catch + Typed Errors + Error State + User Feedback
Core Architecture
- Try-Catch Blocks: Wrap all async operations, external calls, and risky operations
- Error State Management: Track error state in Zustand store (per-slice error state)
- Error Typing: Use
Error type, instanceof Error checks, custom error classes
- User Feedback: Display error messages, loading states, retry buttons
- Error Boundaries: Wrap React components for rendering error handling
- Logging Strategy: Console logging with prefixed tags (
[Service], [Component])
- Environment Handling: Detailed errors in development, user-friendly in production
- Recovery Mechanisms: Retry buttons, reset actions, fallback UI
Principle 1: Try-Catch at Boundaries
- Wrap all async operations (API calls, file I/O, storage access)
- Wrap external library calls that may throw
- Wrap JSON parsing, URL validation, complex computations
- Don't wrap simple synchronous operations (getters, setters)
Principle 2: Error State Management
interface MyFeatureState {
data: MyData | null;
isLoading: boolean;
isSaving: boolean;
error: string | null;
saveError: string | null;
}
saveData: async (data: MyData) => {
const state = get();
set({
myFeature: { ...state.myFeature, isLoading: true, error: null },
});
try {
const result = await apiCall(data);
if (!result.success) throw new Error(result.error.message);
set({
myFeature: {
...state.myFeature,
data: result.data,
isLoading: false,
error: null,
},
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to save data';
set({
myFeature: { ...state.myFeature, isLoading: false, error: errorMessage },
});
throw error;
}
};
Principle 3: Typed Error Handling
export class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = 'ValidationError';
}
}
export class ApiError extends Error {
constructor(
public code: string,
message: string,
public statusCode?: number
) {
super(message);
this.name = 'ApiError';
}
}
catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation failed for ${error.field}: ${error.message}`);
} else if (error instanceof ApiError && error.code === 'AUTH_FAILED') {
}
}
Principle 4: React Error Boundaries
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class MyErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('[MyErrorBoundary]', error, errorInfo);
this.props.onError?.(error, errorInfo);
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(error, {
contexts: { react: { componentStack: errorInfo.componentStack } },
});
}
}
private handleReset = () => {
this.setState({ hasError: false, error: null });
};
override render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div className="error-container">
<h2>Something went wrong</h2>
{process.env.NODE_ENV === 'development' && this.state.error && <pre>{this.state.error.message}</pre>}
<button onClick={this.handleReset}>Try Again</button>
</div>
)
);
}
return this.props.children;
}
}
Principle 5: Exponential Backoff Retry
interface RetryOptions {
maxRetries: number;
initialDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
jitter: boolean;
shouldRetry?: (error: Error, attempt: number) => boolean;
}
export async function retryWithBackoff<T>(fn: () => Promise<T>, options: Partial<RetryOptions> = {}): Promise<T> {
const { maxRetries = 3, initialDelayMs = 1000, maxDelayMs = 30000, backoffMultiplier = 2, jitter = true, shouldRetry = () => true } = options;
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries || !shouldRetry(lastError, attempt)) {
throw lastError;
}
const exponentialDelay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt), maxDelayMs);
const delay = jitter ? exponentialDelay * (0.75 + Math.random() * 0.5) : exponentialDelay;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError!;
}
Retry Decision Matrix
| Error Type | Retry? | Max Retries | Initial Delay |
|---|
| Network timeout | Yes | 3-5 | 1000ms |
| Server error (500-599) | Yes | 3 | 1000ms |
| Rate limit (429) | Yes | 3 | 5000ms |
| Authentication error (401) | No | 0 | N/A |
| Validation error (400) | No | 0 | N/A |
| Not found (404) | No | 0 | N/A |
Retry Policies
const CONSERVATIVE_POLICY = { maxRetries: 2, initialDelayMs: 500, maxDelayMs: 5000 };
const STANDARD_POLICY = { maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 30000 };
const AGGRESSIVE_POLICY = { maxRetries: 5, initialDelayMs: 2000, maxDelayMs: 60000 };
Principle 6: Sentry Production Monitoring
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
enabled: process.env.NODE_ENV === 'production',
tracesSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.01,
beforeSend(event) {
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
}
return event;
},
ignoreErrors: ['ResizeObserver loop limit exceeded', 'Non-Error promise rejection captured'],
});
Sentry.captureException(error, {
tags: { action: 'saveData', feature: 'myFeature' },
extra: { data, state },
});
Sentry.addBreadcrumb({
category: 'user-action',
message: 'element-selected',
level: 'info',
data: { elementId, type },
});
Principle 7: User-Friendly Error Messages
const ERROR_MESSAGES: Record<string, string> = {
ECONNREFUSED: 'Unable to connect to server. Please check your internet connection.',
ETIMEDOUT: 'Request timed out. Please try again.',
AUTH_FAILED: 'Invalid credentials. Please check your email and password.',
FILE_TOO_LARGE: 'File size exceeds 5MB. Please choose a smaller file.',
};
function getUserFriendlyMessage(error: Error): string {
if (error instanceof ApiError && ERROR_MESSAGES[error.code]) {
return ERROR_MESSAGES[error.code];
}
return 'Something went wrong. Please try again.';
}
Logging Best Practices
console.log('[API Slice] Story saved successfully:', { storyId });
console.warn('[Persistence] Failed to load state:', error);
console.error('[Auto-Save] Failed to auto-save story:', errorMessage);
if (process.env.NODE_ENV === 'development') {
console.debug('[State] Canvas zoom updated:', zoom);
}
Graceful Degradation Pattern
try {
const storedValue = localStorage.getItem('key');
if (storedValue) return JSON.parse(storedValue);
} catch (error) {
console.warn('[Storage] Failed to load data:', error);
return null;
}
Error Hook Pattern
export const useErrorHandler = () => {
const [error, setError] = useState<Error | null>(null);
const [isRetrying, setIsRetrying] = useState(false);
const handleError = useCallback((error: Error | string) => {
const errorObj = typeof error === 'string' ? new Error(error) : error;
setError(errorObj);
console.error('[ErrorHandler]', errorObj);
}, []);
const clearError = useCallback(() => {
setError(null);
setIsRetrying(false);
}, []);
const retry = useCallback(
async (retryFn: () => Promise<void> | void) => {
setIsRetrying(true);
try {
await retryFn();
clearError();
} catch (err) {
handleError(err as Error);
} finally {
setIsRetrying(false);
}
},
[handleError, clearError],
);
return { error, isRetrying, handleError, clearError, retry };
};
Testing Error Handling
describe('Error Handling', () => {
it('should set error state when operation fails', async () => {
const { result } = renderHook(() => useMyStore());
mockApi.getData.mockRejectedValue(new Error('API Error'));
await act(async () => {
try {
await result.current.myFeatureActions.loadData();
} catch {
}
});
expect(result.current.myFeature.error).toBe('API Error');
expect(result.current.myFeature.isLoading).toBe(false);
});
it('should clear error on retry success', async () => {
const { result } = renderHook(() => useMyStore());
act(() => result.current.myFeatureActions.setError('Previous error'));
mockApi.getData.mockResolvedValue({ data: 'success' });
await act(async () => {
await result.current.myFeatureActions.loadData();
});
expect(result.current.myFeature.error).toBeNull();
});
});