Error Handling Resilience Implementation
1. React Error Boundaries Fallback UI
| Concept | Scope | Catches | Doesn't Catch |
|---|---|---|---|
| Error Boundary | Component tree below it | Render errors, lifecycle methods, constructors | Event handlers, async code, SSR, errors in boundary itself |
| componentDidCatch | Class component method | Error object, error info with stack | Used for side effects (logging) |
| getDerivedStateFromError | Static lifecycle method | Returns state to render fallback | Pure function, no side effects |
| react-error-boundary BEST | Third-party library | Hooks support, reset, FallbackComponent | More features than built-in |
Example: Error Boundaries Implementation
// Basic Error Boundary (class component)
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Update state to render fallback UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to service
console.error('Error caught by boundary:', error, errorInfo);
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}
// react-error-boundary library (recommended)
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset app state
window.location.reload();
}}
onError={(error, errorInfo) => {
// Log to error reporting service
logErrorToSentry(error, errorInfo);
}}
>
<MyApp />
</ErrorBoundary>
);
}
// Multiple error boundaries (granular)
function App() {
return (
<ErrorBoundary FallbackComponent={AppErrorFallback}>
<Header />
<ErrorBoundary FallbackComponent={SidebarErrorFallback}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={MainErrorFallback}>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
);
}
// Error boundary with reset keys
function UserProfile({ userId }) {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[userId]} // Reset when userId changes
>
<UserData userId={userId} />
</ErrorBoundary>
);
}
// Custom error boundary hook
function useErrorHandler() {
const [error, setError] = useState(null);
useEffect(() => {
if (error) {
throw error; // Will be caught by Error Boundary
}
}, [error]);
return setError;
}
// Usage in async code
function MyComponent() {
const handleError = useErrorHandler();
const fetchData = async () => {
try {
const data = await api.getData();
setData(data);
} catch (error) {
handleError(error); // Throw to Error Boundary
}
};
return <button onClick={fetchData}>Fetch Data</button>;
}
// Error boundary with different fallbacks per error type
function ErrorFallback({ error, resetErrorBoundary }) {
if (error.name === 'ChunkLoadError') {
return (
<div>
<h2>New version available</h2>
<button onClick={() => window.location.reload()}>
Reload to update
</button>
</div>
);
}
if (error.message.includes('Network')) {
return (
<div>
<h2>Network Error</h2>
<p>Check your internet connection</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}
return (
<div>
<h2>Application Error</h2>
<details>
<summary>Error details</summary>
<pre>{error.stack}</pre>
</details>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Nested error boundaries with context
const ErrorContext = createContext();
function RootErrorBoundary({ children }) {
const [errors, setErrors] = useState([]);
const logError = (error, errorInfo) => {
setErrors(prev => [...prev, { error, errorInfo, timestamp: Date.now() }]);
};
return (
<ErrorContext.Provider value={{ errors, logError }}>
<ErrorBoundary
FallbackComponent={RootErrorFallback}
onError={logError}
>
{children}
</ErrorBoundary>
</ErrorContext.Provider>
);
}
// Testing error boundaries
// MyComponent.test.tsx
import { render, screen } from '@testing-library/react';
import { ErrorBoundary } from 'react-error-boundary';
const ThrowError = () => {
throw new Error('Test error');
};
test('shows error fallback', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
render(
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
consoleSpy.mockRestore();
});
// Global error handler for unhandled errors
window.addEventListener('error', (event) => {
console.error('Unhandled error:', event.error);
logErrorToService(event.error);
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
logErrorToService(event.reason);
});
// Suspense with Error Boundary
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
// Error boundary for specific routes (React Router)
function AppRoutes() {
return (
<Routes>
<Route
path="/"
element={
<ErrorBoundary FallbackComponent={HomeErrorFallback}>
<Home />
</ErrorBoundary>
}
/>
<Route
path="/dashboard"
element={
<ErrorBoundary FallbackComponent={DashboardErrorFallback}>
<Dashboard />
</ErrorBoundary>
}
/>
</Routes>
);
}
Limitation: Error boundaries don't catch errors in event handlers,
async code, SSR, or the boundary itself. Use try-catch for those.
2. Sentry LogRocket Error Monitoring
| Tool | Type | Features | Use Case |
|---|---|---|---|
| Sentry BEST | Error tracking | Stack traces, breadcrumbs, releases, performance | Real-time error monitoring, source maps, alerts |
| LogRocket | Session replay | Video replay, console logs, network, Redux | Debug user sessions, understand context |
| Rollbar | Error tracking | Real-time alerts, telemetry, people tracking | Similar to Sentry, alternative option |
| Bugsnag | Error monitoring | Stability scoring, release health | Mobile + web error tracking |
Example: Error Monitoring Setup
// Sentry setup
npm install @sentry/react @sentry/tracing
// index.tsx
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
environment: process.env.NODE_ENV,
integrations: [
new BrowserTracing(),
new Sentry.Replay({
maskAllText: false,
blockAllMedia: false,
})
],
// Performance monitoring
tracesSampleRate: 1.0, // 100% in dev, 0.1 (10%) in prod
// Session replay
replaysSessionSampleRate: 0.1, // 10% of sessions
replaysOnErrorSampleRate: 1.0, // 100% of errors
// Release tracking
release: process.env.REACT_APP_VERSION,
// Filter errors
beforeSend(event, hint) {
// Don't send to Sentry in dev
if (process.env.NODE_ENV === 'development') {
return null;
}
// Filter out specific errors
if (event.exception?.values?.[0]?.value?.includes('ResizeObserver')) {
return null;
}
return event;
}
});
// Wrap app with Sentry
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<Sentry.ErrorBoundary fallback={ErrorFallback} showDialog>
<App />
</Sentry.ErrorBoundary>
);
// React Router integration
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from 'react-router-dom';
Sentry.init({
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
React.useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes
)
})
]
});
// Manual error reporting
try {
somethingThatMightFail();
} catch (error) {
Sentry.captureException(error);
}
// Add context to errors
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username
});
Sentry.setTag('page_locale', 'en-us');
Sentry.setContext('character', {
name: 'Mighty Fighter',
level: 19,
gold: 12300
});
// Breadcrumbs (automatic + manual)
Sentry.addBreadcrumb({
category: 'auth',
message: 'User logged in',
level: 'info'
});
// Capture custom messages
Sentry.captureMessage('Something important happened', 'warning');
// Performance monitoring
const transaction = Sentry.startTransaction({
name: 'API Request',
op: 'http.client'
});
fetch('/api/data')
.then(response => response.json())
.finally(() => transaction.finish());
// React profiler integration
import { Profiler } from '@sentry/react';
function App() {
return (
<Profiler name="App">
<MyComponent />
</Profiler>
);
}
// LogRocket setup
npm install logrocket logrocket-react
import LogRocket from 'logrocket';
import setupLogRocketReact from 'logrocket-react';
LogRocket.init('YOUR_APP_ID', {
console: {
shouldAggregateConsoleErrors: true
},
network: {
requestSanitizer: (request) => {
// Remove sensitive data
if (request.headers['Authorization']) {
request.headers['Authorization'] = '[REDACTED]';
}
return request;
}
},
dom: {
inputSanitizer: true // Mask input values
}
});
setupLogRocketReact(LogRocket);
// Identify user
LogRocket.identify(user.id, {
name: user.name,
email: user.email,
subscriptionType: 'pro'
});
// Track custom events
LogRocket.track('Checkout Completed', {
orderId: '12345',
total: 99.99
});
// Sentry + LogRocket integration
LogRocket.getSessionURL((sessionURL) => {
Sentry.configureScope((scope) => {
scope.setExtra('sessionURL', sessionURL);
});
});
// Add LogRocket to Sentry events
Sentry.init({
beforeSend(event) {
if (event.exception) {
const sessionURL = LogRocket.sessionURL;
event.extra = {
...event.extra,
LogRocket: sessionURL
};
}
return event;
}
});
// Redux integration
import LogRocket from 'logrocket';
const store = createStore(
reducer,
applyMiddleware(LogRocket.reduxMiddleware())
);
// Source maps for Sentry
// package.json
{
"scripts": {
"build": "react-scripts build && sentry-cli releases files upload-sourcemaps ./build"
}
}
// .sentryclirc
[defaults]
url=https://sentry.io/
org=your-org
project=your-project
[auth]
token=YOUR_AUTH_TOKEN
// Next.js Sentry config
// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(
{
// Next.js config
},
{
silent: true,
org: 'your-org',
project: 'your-project'
}
);
// Error monitoring best practices
// 1. Set user context
Sentry.setUser({ id: user.id, email: user.email });
// 2. Add tags for filtering
Sentry.setTag('environment', 'production');
Sentry.setTag('feature', 'checkout');
// 3. Add breadcrumbs
Sentry.addBreadcrumb({
message: 'Button clicked',
category: 'ui.click',
level: 'info'
});
// 4. Filter noise
beforeSend(event) {
// Ignore known third-party errors
if (event.exception?.values?.[0]?.value?.includes('third-party-script')) {
return null;
}
return event;
}
// 5. Sample rates
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Custom error class
class ApplicationError extends Error {
constructor(message, code, context) {
super(message);
this.name = 'ApplicationError';
this.code = code;
this.context = context;
}
}
// Report with context
try {
throw new ApplicationError('Payment failed', 'PAYMENT_ERROR', {
userId: user.id,
amount: 99.99
});
} catch (error) {
Sentry.captureException(error);
}
Industry Standard: 85% of top companies use Sentry. 1M+
developers. Free tier covers small projects. Essential for production apps.
3. Try-Catch Async Error Handling
| Pattern | Use Case | Pros | Cons |
|---|---|---|---|
| Try-Catch | Sync code, async/await | Simple, catches synchronous errors | Verbose, doesn't catch all async errors |
| Promise.catch() | Promise chains | Chainable, handles async rejections | Can be verbose with multiple catches |
| Global handlers | Unhandled errors/rejections | Catches everything as last resort | Should not be primary error handling |
| Error wrapper | Consistent API error handling | DRY, centralized logic | Requires setup |
Example: Async Error Handling Patterns
// Basic try-catch with async/await
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error; // Re-throw or handle
}
}
// Promise chain with catch
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('Fetch failed:', error);
throw error;
});
}
// React component with error handling
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const data = await fetchUserData(userId);
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user.name}</div>;
}
// Custom error classes
class APIError extends Error {
constructor(message, statusCode, response) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
this.response = response;
}
}
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(message, fields) {
super(message);
this.name = 'ValidationError';
this.fields = fields;
}
}
// API wrapper with error handling
async function apiRequest(url, options = {}) {
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
const data = await response.json();
if (!response.ok) {
throw new APIError(
data.message || 'API request failed',
response.status,
data
);
}
return data;
} catch (error) {
if (error instanceof APIError) {
throw error;
}
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new NetworkError('Network connection failed');
}
throw error;
}
}
// Usage with specific error handling
async function login(email, password) {
try {
const data = await apiRequest('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
return data;
} catch (error) {
if (error instanceof APIError) {
if (error.statusCode === 401) {
throw new Error('Invalid credentials');
}
if (error.statusCode === 429) {
throw new Error('Too many attempts. Try again later.');
}
}
if (error instanceof NetworkError) {
throw new Error('No internet connection');
}
throw new Error('Login failed. Please try again.');
}
}
// React Query error handling
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUserData(userId),
retry: (failureCount, error) => {
// Don't retry on 404
if (error.statusCode === 404) return false;
return failureCount < 3;
},
onError: (error) => {
console.error('Query failed:', error);
toast.error(error.message);
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{data.name}</div>;
}
// Axios interceptor for error handling
import axios from 'axios';
const api = axios.create({
baseURL: '/api'
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Redirect to login
window.location.href = '/login';
}
if (error.response?.status === 403) {
toast.error('Permission denied');
}
if (error.response?.status >= 500) {
toast.error('Server error. Please try again.');
}
return Promise.reject(error);
}
);
// Global error handlers
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
// Report to monitoring service
Sentry.captureException(event.error);
// Show user-friendly message
toast.error('Something went wrong');
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
Sentry.captureException(event.reason);
toast.error('An unexpected error occurred');
});
// Async error wrapper utility
function asyncHandler(fn) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
console.error('Async handler caught error:', error);
throw error;
}
};
}
// Usage
const safeFunction = asyncHandler(async (userId) => {
const data = await fetchUserData(userId);
return data;
});
// Error handling in event handlers
function MyComponent() {
const handleClick = async () => {
try {
await performAction();
toast.success('Action completed');
} catch (error) {
console.error('Action failed:', error);
toast.error(error.message);
}
};
return <button onClick={handleClick}>Click me</button>;
}
// Parallel requests with error handling
async function fetchAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
} catch (error) {
// If any request fails, all fail
console.error('One or more requests failed:', error);
throw error;
}
}
// Parallel with individual error handling
async function fetchAllDataSafe() {
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
const data = {
users: results[0].status === 'fulfilled' ? results[0].value : [],
posts: results[1].status === 'fulfilled' ? results[1].value : [],
comments: results[2].status === 'fulfilled' ? results[2].value : []
};
const errors = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (errors.length > 0) {
console.error('Some requests failed:', errors);
}
return data;
}
// TypeScript error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function safeFetch<T>(url: string): Promise<Result<T>> {
try {
const response = await fetch(url);
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: error as Error };
}
}
// Usage
const result = await safeFetch<User>('/api/user');
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
Error Recovery: 80% of errors are recoverable with retry logic.
Implement exponential backoff for transient failures.
4. Toast Notifications User Feedback
| Library | Bundle Size | Features | Use Case |
|---|---|---|---|
| react-hot-toast | ~5KB | Lightweight, customizable, promise API | Modern, minimal, best DX |
| react-toastify | ~15KB | Feature-rich, progress bars, auto-close | Most popular, full-featured |
| sonner HOT | ~3KB | Radix UI based, accessible, beautiful | New, modern, best design |
| notistack | ~20KB | Material-UI integration, stacking | MUI projects, advanced features |
Example: Toast Notifications Implementation
// react-hot-toast
npm install react-hot-toast
import toast, { Toaster } from 'react-hot-toast';
function App() {
return (
<>
<Toaster position="top-right" />
<MyApp />
</>
);
}
// Basic usage
const notify = () => toast('Hello World');
const success = () => toast.success('Saved successfully');
const error = () => toast.error('Failed to save');
const loading = () => toast.loading('Loading...');
// Promise-based toast
const saveData = async () => {
toast.promise(
api.saveData(),
{
loading: 'Saving...',
success: 'Saved successfully',
error: 'Failed to save'
}
);
};
// Custom duration
toast.success('Saved', { duration: 5000 });
// Custom styling
toast.success('Success', {
style: {
background: '#28a745',
color: '#fff'
},
icon: '✅'
});
// Dismissible toast with action
toast((t) => (
<div>
<p>Item deleted</p>
<button onClick={() => {
undoDelete();
toast.dismiss(t.id);
}}>
Undo
</button>
</div>
), {
duration: 5000
});
// Custom toast component
toast.custom((t) => (
<div
className={`custom-toast ${t.visible ? 'animate-enter' : 'animate-leave'}`}
>
Custom content
</div>
));
// Sonner (modern, accessible)
npm install sonner
import { Toaster, toast } from 'sonner';
function App() {
return (
<>
<Toaster position="bottom-right" richColors />
<MyApp />
</>
);
}
// Usage
toast('Event created');
toast.success('Successfully saved');
toast.error('Something went wrong');
toast.warning('Be careful');
toast.info('Update available');
// With description
toast.success('Success', {
description: 'Your changes have been saved'
});
// With action button
toast('Event created', {
action: {
label: 'Undo',
onClick: () => undoAction()
}
});
// Promise with loading state
toast.promise(fetchData(), {
loading: 'Loading...',
success: (data) => `${data.name} loaded`,
error: 'Failed to load'
});
// react-toastify
npm install react-toastify
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
function App() {
return (
<>
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop
closeOnClick
pauseOnHover
/>
<MyApp />
</>
);
}
// Usage
toast.success('Success message');
toast.error('Error message');
toast.warn('Warning message');
toast.info('Info message');
// With custom options
toast.success('Saved', {
position: 'bottom-center',
autoClose: 3000,
hideProgressBar: true
});
// Update existing toast
const toastId = toast.loading('Uploading...');
// Later
toast.update(toastId, {
render: 'Upload complete',
type: 'success',
isLoading: false,
autoClose: 3000
});
// Custom component
const CustomToast = ({ closeToast, message }) => (
<div>
<p>{message}</p>
<button onClick={closeToast}>Close</button>
</div>
);
toast(<CustomToast message="Hello" />);
// Toast notification patterns
// Success toast after API call
const handleSave = async () => {
try {
await api.saveData(data);
toast.success('Data saved successfully');
} catch (error) {
toast.error('Failed to save data');
}
};
// Loading state with promise
const handleSubmit = async () => {
const promise = api.submitForm(formData);
toast.promise(promise, {
loading: 'Submitting...',
success: 'Form submitted successfully',
error: (err) => `Error: ${err.message}`
});
};
// Undo action toast
const handleDelete = (itemId) => {
const deletedItem = items.find(i => i.id === itemId);
// Optimistically remove
setItems(items.filter(i => i.id !== itemId));
// Show undo toast
toast((t) => (
<div>
<span>Item deleted</span>
<button
onClick={() => {
setItems([...items, deletedItem]);
toast.dismiss(t.id);
}}
>
Undo
</button>
</div>
), {
duration: 5000
});
// Permanently delete after timeout
setTimeout(() => {
api.deleteItem(itemId);
}, 5000);
};
// Multiple toasts with limit
const MAX_TOASTS = 3;
function showToast(message) {
if (toast.visibleToasts().length >= MAX_TOASTS) {
return;
}
toast(message);
}
// Toast with React Query
import { useMutation } from '@tanstack/react-query';
function MyComponent() {
const mutation = useMutation({
mutationFn: saveData,
onSuccess: () => {
toast.success('Saved successfully');
},
onError: (error) => {
toast.error(error.message);
}
});
return (
<button onClick={() => mutation.mutate(data)}>
Save
</button>
);
}
// Global toast wrapper
export const notify = {
success: (message) => toast.success(message),
error: (message) => toast.error(message),
info: (message) => toast.info(message),
warning: (message) => toast.warning(message),
promise: (promise, messages) => toast.promise(promise, messages)
};
// Usage throughout app
import { notify } from './utils/toast';
notify.success('Operation completed');
// Toast accessibility
<Toaster
position="top-right"
toastOptions={{
role: 'status',
ariaLive: 'polite'
}}
/>
// Custom toast with close button
toast.custom((t) => (
<div role="alert" aria-live="assertive">
<p>{message}</p>
<button
onClick={() => toast.dismiss(t.id)}
aria-label="Close notification"
>
×
</button>
</div>
));
User Experience: Toast notifications provide non-intrusive
feedback. Don't overuse - limit to 3 visible at once.
5. Retry Logic Exponential Backoff
| Strategy | Wait Time | Use Case | Example |
|---|---|---|---|
| Fixed Delay | Same delay between retries | Simple, non-critical operations | 1s, 1s, 1s |
| Linear Backoff | Linearly increasing delay | Moderate load scenarios | 1s, 2s, 3s |
| Exponential Backoff | Exponentially increasing delay | Rate-limited APIs, best practice | 1s, 2s, 4s, 8s |
| Exponential + Jitter | Exponential with randomization | Prevent thundering herd, production | 1s, 2.3s, 4.7s |
Example: Retry Logic Implementation
// Simple retry function
async function retry(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await retry(() => fetchData(), 3, 1000);
// Exponential backoff
async function retryWithExponentialBackoff(
fn,
maxAttempts = 5,
baseDelay = 1000,
maxDelay = 30000
) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
// Exponential: 1s, 2s, 4s, 8s, 16s
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
console.log(`Attempt ${attempt} failed, waiting ${delay}ms before retry`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Exponential backoff with jitter (prevents thundering herd)
function calculateBackoffWithJitter(attempt, baseDelay = 1000, maxDelay = 30000) {
const exponentialDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
const jitter = Math.random() * exponentialDelay * 0.3; // ±30% jitter
return exponentialDelay + jitter;
}
async function retryWithJitter(fn, maxAttempts = 5) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
const delay = calculateBackoffWithJitter(attempt);
console.log(`Retry in ${Math.round(delay)}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Conditional retry (only for specific errors)
async function retryOnError(fn, shouldRetry, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts || !shouldRetry(error)) {
throw error;
}
const delay = 1000 * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage: Only retry on network errors
const data = await retryOnError(
fetchData,
(error) => error.name === 'NetworkError' || error.statusCode >= 500,
3
);
// React Query with retry
import { useQuery } from '@tanstack/react-query';
function MyComponent() {
const { data, error, isLoading } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
});
}
// Axios with retry interceptor
npm install axios-retry
import axios from 'axios';
import axiosRetry from 'axios-retry';
const api = axios.create({
baseURL: '/api'
});
axiosRetry(api, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429; // Rate limit
}
});
// SWR with retry
import useSWR from 'swr';
function Profile() {
const { data, error } = useSWR('/api/user', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// Don't retry on 404
if (error.status === 404) return;
// Max 5 retries
if (retryCount >= 5) return;
// Exponential backoff
setTimeout(() => revalidate({ retryCount }), 1000 * Math.pow(2, retryCount));
}
});
}
// Fetch with retry wrapper
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
const { retry = maxRetries, retryDelay = 1000, ...fetchOptions } = options;
for (let attempt = 0; attempt <= retry; attempt++) {
try {
const response = await fetch(url, fetchOptions);
// Retry on 5xx errors
if (response.status >= 500 && attempt < retry) {
throw new Error(`Server error: ${response.status}`);
}
return response;
} catch (error) {
if (attempt === retry) {
throw error;
}
const delay = retryDelay * Math.pow(2, attempt);
console.log(`Retry ${attempt + 1}/${retry} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const response = await fetchWithRetry('/api/data', {
retry: 5,
retryDelay: 1000
});
// Retry with abort controller (cancellable)
async function retryWithAbort(fn, signal, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (signal?.aborted) {
throw new Error('Request aborted');
}
try {
return await fn(signal);
} catch (error) {
if (attempt === maxAttempts || signal?.aborted) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
// Usage
const controller = new AbortController();
try {
const data = await retryWithAbort(
(signal) => fetch('/api/data', { signal }),
controller.signal,
3
);
} catch (error) {
console.error('Failed after retries:', error);
}
// Cancel if needed
controller.abort();
// React hook for retry
function useRetry(fn, options = {}) {
const { maxAttempts = 3, baseDelay = 1000 } = options;
const [attempt, setAttempt] = useState(0);
const [isRetrying, setIsRetrying] = useState(false);
const execute = async () => {
for (let i = 1; i <= maxAttempts; i++) {
try {
setAttempt(i);
setIsRetrying(i > 1);
const result = await fn();
setIsRetrying(false);
return result;
} catch (error) {
if (i === maxAttempts) {
setIsRetrying(false);
throw error;
}
const delay = baseDelay * Math.pow(2, i - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};
return { execute, attempt, isRetrying };
}
// Usage
function MyComponent() {
const { execute, attempt, isRetrying } = useRetry(fetchData, {
maxAttempts: 5,
baseDelay: 1000
});
const handleClick = async () => {
try {
const data = await execute();
console.log('Success:', data);
} catch (error) {
console.error('Failed after retries:', error);
}
};
return (
<div>
<button onClick={handleClick} disabled={isRetrying}>
Fetch Data
</button>
{isRetrying && <p>Retrying... (Attempt {attempt})</p>}
</div>
);
}
// Retry with progress callback
async function retryWithProgress(fn, onProgress, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
onProgress({ attempt, status: 'trying' });
const result = await fn();
onProgress({ attempt, status: 'success' });
return result;
} catch (error) {
onProgress({ attempt, status: 'failed', error });
if (attempt === maxAttempts) {
throw error;
}
const delay = 1000 * Math.pow(2, attempt - 1);
onProgress({ attempt, status: 'waiting', delay });
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
AWS Best Practice: Use exponential backoff with jitter to
prevent thundering herd. AWS SDKs implement this by default.
6. Circuit Breaker Pattern Frontend
| State | Behavior | Transition | Purpose |
|---|---|---|---|
| Closed | Normal operation, requests pass through | Open after N failures | Allow traffic when healthy |
| Open | Fail fast, reject requests immediately | Half-Open after timeout | Prevent cascading failures |
| Half-Open | Test with limited requests | Closed on success, Open on failure | Gradual recovery testing |
Example: Circuit Breaker Implementation
// Circuit breaker class
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.successThreshold = options.successThreshold || 2;
this.timeout = options.timeout || 60000; // 60s
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
// Try half-open
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= this.successThreshold) {
this.state = 'CLOSED';
this.successCount = 0;
}
}
}
onFailure() {
this.failureCount++;
this.successCount = 0;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
getState() {
return this.state;
}
reset() {
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
}
}
// Usage
const breaker = new CircuitBreaker({
failureThreshold: 5,
successThreshold: 2,
timeout: 60000
});
async function fetchData() {
try {
return await breaker.execute(() => fetch('/api/data'));
} catch (error) {
if (error.message === 'Circuit breaker is OPEN') {
// Use cached data or show error
return getCachedData();
}
throw error;
}
}
// React hook for circuit breaker
function useCircuitBreaker(options) {
const breakerRef = useRef(new CircuitBreaker(options));
const [state, setState] = useState('CLOSED');
const execute = useCallback(async (fn) => {
try {
const result = await breakerRef.current.execute(fn);
setState(breakerRef.current.getState());
return result;
} catch (error) {
setState(breakerRef.current.getState());
throw error;
}
}, []);
const reset = useCallback(() => {
breakerRef.current.reset();
setState('CLOSED');
}, []);
return { execute, state, reset };
}
// Usage in component
function DataFetcher() {
const { execute, state, reset } = useCircuitBreaker({
failureThreshold: 3,
timeout: 30000
});
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const fetchData = async () => {
try {
const result = await execute(() => api.getData());
setData(result);
setError(null);
} catch (err) {
setError(err.message);
}
};
return (
<div>
<p>Circuit State: {state}</p>
{state === 'OPEN' && (
<div>
<p>Service unavailable. Using cached data.</p>
<button onClick={reset}>Reset Circuit</button>
</div>
)}
<button onClick={fetchData} disabled={state === 'OPEN'}>
Fetch Data
</button>
{error && <p>Error: {error}</p>}
</div>
);
}
// Circuit breaker with fallback
class CircuitBreakerWithFallback extends CircuitBreaker {
constructor(options = {}) {
super(options);
this.fallback = options.fallback || (() => null);
}
async execute(fn) {
try {
return await super.execute(fn);
} catch (error) {
if (this.state === 'OPEN') {
return this.fallback();
}
throw error;
}
}
}
// Usage with fallback
const breaker = new CircuitBreakerWithFallback({
failureThreshold: 5,
timeout: 60000,
fallback: () => getCachedData()
});
// API service with circuit breaker
class APIService {
constructor() {
this.breakers = new Map();
}
getBreaker(endpoint) {
if (!this.breakers.has(endpoint)) {
this.breakers.set(endpoint, new CircuitBreaker({
failureThreshold: 3,
timeout: 30000
}));
}
return this.breakers.get(endpoint);
}
async request(endpoint, options) {
const breaker = this.getBreaker(endpoint);
return breaker.execute(async () => {
const response = await fetch(endpoint, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
}
}
// Usage
const api = new APIService();
try {
const data = await api.request('/api/users');
} catch (error) {
console.error('Request failed:', error);
}
// Circuit breaker with metrics
class CircuitBreakerWithMetrics extends CircuitBreaker {
constructor(options = {}) {
super(options);
this.metrics = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
rejectedRequests: 0
};
}
async execute(fn) {
this.metrics.totalRequests++;
if (this.state === 'OPEN' && Date.now() < this.nextAttempt) {
this.metrics.rejectedRequests++;
throw new Error('Circuit breaker is OPEN');
}
try {
const result = await super.execute(fn);
this.metrics.successfulRequests++;
return result;
} catch (error) {
this.metrics.failedRequests++;
throw error;
}
}
getMetrics() {
return {
...this.metrics,
successRate: this.metrics.successfulRequests / this.metrics.totalRequests
};
}
}
// Monitoring dashboard
function CircuitBreakerDashboard({ breaker }) {
const [metrics, setMetrics] = useState(breaker.getMetrics());
useEffect(() => {
const interval = setInterval(() => {
setMetrics(breaker.getMetrics());
}, 1000);
return () => clearInterval(interval);
}, [breaker]);
return (
<div>
<h3>Circuit Breaker Status</h3>
<p>State: {breaker.getState()}</p>
<p>Total Requests: {metrics.totalRequests}</p>
<p>Success Rate: {(metrics.successRate * 100).toFixed(2)}%</p>
<p>Rejected: {metrics.rejectedRequests}</p>
</div>
);
}
// Circuit breaker pattern with multiple services
const breakers = {
userService: new CircuitBreaker({ failureThreshold: 3 }),
paymentService: new CircuitBreaker({ failureThreshold: 5 }),
notificationService: new CircuitBreaker({ failureThreshold: 2 })
};
async function fetchUser(id) {
return breakers.userService.execute(() => api.getUser(id));
}
async function processPayment(data) {
return breakers.paymentService.execute(() => api.processPayment(data));
}
// Global circuit breaker state management
const CircuitBreakerContext = createContext();
function CircuitBreakerProvider({ children }) {
const [breakers] = useState(() => ({
api: new CircuitBreakerWithMetrics({ failureThreshold: 5 })
}));
return (
<CircuitBreakerContext.Provider value={breakers}>
{children}
</CircuitBreakerContext.Provider>
);
}
function useCircuitBreakerAPI() {
const breakers = useContext(CircuitBreakerContext);
return breakers.api;
}
Error Handling & Resilience Summary
- Error Boundaries: Catch React render errors. Use react-error-boundary library. Granular boundaries per feature. Don't catch async/event errors
- Monitoring: Sentry for error tracking (5K free/month). LogRocket for session replay. Source maps for production debugging
- Try-Catch: Wrap async/await, create custom error classes, centralize API errors, use global handlers as fallback
- Toast Notifications: react-hot-toast (5KB) or Sonner (3KB). Auto-dismiss 3-5s. Limit to 3 toasts. Add undo for destructive actions
- Retry Logic: Exponential backoff with jitter (1s, 2s, 4s, 8s). Retry 5xx errors, network failures, 429 rate limits. Don't retry 4xx
- Circuit Breaker: Fail fast when service down. 3 states: Closed→Open→Half-Open. Prevent cascading failures, enable fallbacks
Production Ready: Implement all 6 patterns for production apps.
99.9% uptime requires resilience. Users expect 200ms response or fallback.