# error-handling > WHAT: Error boundaries with fallback UI and OpenTelemetry span recording. WHEN: isolating component errors, handling GraphQL failures, implementing retry logic. KEYWORDS: ErrorBoundary, error handling, fallback UI, OpenTelemetry, recordException, retry, GraphQL, errorPolicy, scope. - Author: João Prado - Repository: guicheffer/devorch-cli - Version: 20260122234748 - Stars: 1 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/guicheffer/devorch-cli - Web: https://mule.run/skillshub/@@guicheffer/devorch-cli~error-handling:20260122234748 --- --- name: error-handling description: "WHAT: Error boundaries with fallback UI and OpenTelemetry span recording. WHEN: isolating component errors, handling GraphQL failures, implementing retry logic. KEYWORDS: ErrorBoundary, error handling, fallback UI, OpenTelemetry, recordException, retry, GraphQL, errorPolicy, scope." --- # Error Handling & Recovery Patterns ## Documentation This skill has comprehensive documentation: - **[Production Examples](./references/examples.md)** - Real-world code examples from the codebase - **[API Reference](./references/api-docs.md)** - Complete API documentation with official links - **[Implementation Patterns](./references/patterns.md)** - Best practices and anti-patterns ## Core Principles **Use ErrorBoundary components to isolate errors and prevent app crashes.** Integrate OpenTelemetry spans to track errors in distributed tracing and provide user-friendly fallback UI with recovery actions. **Why**: Proper error handling prevents entire app crashes, improves user experience with actionable error messages, and enables debugging production issues through distributed tracing. ## When to Use This Skill Use these patterns when: - Wrapping feature components to isolate errors - Integrating error tracking with OpenTelemetry - Creating fallback UI for error states - Handling GraphQL query/mutation errors - Implementing retry logic for transient failures - Testing error boundaries and error handling paths - Tracking errors in production with distributed tracing ## ErrorBoundary Component ### Component-Level Boundaries Wrap features with ErrorBoundary to isolate errors and prevent app-wide crashes: ```typescript import { ErrorBoundary } from '@libs/error-boundary'; export const RecipeListScreen = () => { return ( ( )} > ); }; ``` **Key patterns:** - `scope.moduleName`: Identifies where error occurred (required) - `scope.attributes`: Additional context for debugging (optional) - `fallback`: Custom error UI with retry functionality - Automatic OpenTelemetry span creation with `recordException()` **Why**: Component-level boundaries prevent one failing component from crashing the entire app. Errors are isolated, logged to OpenTelemetry, and users see helpful recovery UI. **Production Example**: `git-resources/shared-mobile-modules/src/modules/comms/screens/inbox/InboxScreen.tsx:1` ### Screen-Level Boundaries Wrap entire app or navigation stacks with top-level ErrorBoundary: ```typescript import { ErrorBoundary } from '@libs/error-boundary'; export const ScreenEntryProvider: React.FC = ({ children }) => { return ( {children} ); }; ``` **Why**: Top-level boundaries catch errors that escape component boundaries, ensuring the app never shows a blank white screen. **Production Example**: `git-resources/shared-mobile-modules/src/entry-providers/providers.tsx:140` ### Error Scope Configuration Always provide scope information for debugging: ```typescript { // Optional: Custom error handling logError(error, { moduleName: 'Checkout', componentStack }); }} > ``` **Key patterns:** - `moduleName`: Required identifier (e.g., 'Checkout', 'RecipeList') - `attributes`: OpenTelemetry attributes for filtering/grouping errors - `onError`: Optional callback for custom error handling **Why**: Rich scope data helps identify error sources in production logs and enables filtering errors by team, feature, or screen. **Production Example**: `git-resources/shared-mobile-modules/src/libs/error-boundary/ErrorBoundary.tsx:76` ## OpenTelemetry Integration ### Span Recording with Exceptions Always use try-catch-finally pattern with spans: ```typescript import { useTracer } from '@libs/observability'; import { SpanStatusCode } from '@opentelemetry/api'; export const useCheckoutFlow = () => { const { startSpan } = useTracer(); const processPayment = async (paymentData: PaymentData) => { const span = startSpan('checkout.processPayment', { attributes: { 'payment.method': paymentData.method, 'cart.total': paymentData.total, 'payment.currency': paymentData.currency, }, }); try { const result = await paymentService.process(paymentData); span.setStatus({ code: SpanStatusCode.OK }); span.setAttributes({ 'payment.transaction_id': result.transactionId, 'payment.status': 'success', }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message, }); span.recordException(error as Error); span.setAttributes({ 'payment.status': 'failed', 'error.type': (error as Error).name, }); throw error; // Re-throw for caller to handle } finally { span.end(); // ALWAYS end span } }; return { processPayment }; }; ``` **Key patterns:** - `startSpan()` with descriptive name (`checkout.processPayment`) - `setStatus()` with OK or ERROR - `recordException()` for detailed error tracking - `span.end()` in finally block (CRITICAL) - Attributes for request context and results **Why**: Distributed tracing enables debugging complex async flows in production. Always ending spans in finally blocks prevents memory leaks. **Production Example**: `git-resources/shared-mobile-modules/src/libs/networking-client/client/useFetch.ts:89` ### Automatic GraphQL Tracing GraphQL operations automatically create spans via tracingLink: ```typescript // Automatic instrumentation in Apollo Client setup import { tracingLink } from '@libs/graphql/links/tracing'; const apolloClient = new ApolloClient({ link: ApolloLink.from([ tracingLink, // Automatically creates spans for all GraphQL operations httpLink, ]), cache: inMemoryCache, }); ``` **How tracingLink works:** - Creates span for each GraphQL operation: `QUERY GetRecipes`, `MUTATION CreateOrder` - Adds operation type, name, variables (sanitized) - Records GraphQL errors with `recordException()` - Sets span attributes: response status, error count, error messages - Automatically ends span on completion **Why**: Consistent telemetry for all GraphQL operations without manual instrumentation. Errors are automatically tracked in OpenTelemetry. **Production Example**: `git-resources/shared-mobile-modules/src/libs/graphql/links/tracing.ts:115` ## Fallback UI Patterns ### User-Friendly Error Messages Provide clear, actionable error messages based on error type: ```typescript export const ErrorFallback = ({ error, onRetry }: ErrorFallbackProps) => { const styles = useZestStyles(stylesConfig); // Network errors if (error.networkError || error.message.includes('Network request failed')) { return ( No Internet Connection Please check your connection and try again. ); } // Authentication errors if (error.message.includes('unauthorized') || error.message.includes('401')) { return ( Session Expired Your session has expired. Please sign in again. ); } // Generic errors return ( Something Went Wrong We encountered an unexpected error. ); }; ``` **Key patterns:** - Detect error type from error properties or message - Show appropriate icon and message for each error type - Always provide action (Retry, Sign In Again) - Never show technical stack traces to users **Why**: User-friendly messages improve UX and guide users toward recovery actions. Technical errors confuse non-technical users. ### Development vs Production Fallbacks Show different UI in development vs production: ```typescript export const DefaultErrorFallback = ({ error, errorInfo, resetError, }: DefaultErrorFallbackProps) => { if (__DEV__) { return ( ); } return ( ); }; ``` **Development fallback:** - Shows full error message and stack trace - Displays component stack for debugging - Helpful for developers during development **Production fallback:** - Shows generic user-friendly message - Hides technical details - Provides retry action **Why**: Developers need detailed error info for debugging. Users need simple, actionable messages without technical jargon. **Production Example**: `git-resources/shared-mobile-modules/src/libs/error-boundary/fallback-ui/DefaultErrorFallback.tsx:1` ## GraphQL Error Handling ### Error Policy Configuration Use `errorPolicy` to control how GraphQL errors are handled: ```typescript export const useGetProductDetails = (productId: string) => { const { data, loading, error } = useQuery(GetProductDetailsDocument, { variables: { productId }, errorPolicy: 'all', // Return partial data + errors }); const hasErrors = error?.graphQLErrors.length > 0; const hasNetworkError = error?.networkError != null; return { product: data?.product || null, loading: loading && !data, hasErrors, hasNetworkError, }; }; ``` **ErrorPolicy options:** - `none` (default): Throw on any GraphQL error, no data returned - `ignore`: Ignore errors entirely, return data only - `all`: Return partial data + errors (RECOMMENDED) **Why**: `errorPolicy: 'all'` enables graceful degradation. Show partial data when available instead of blank error screen. **Production Example**: `git-resources/shared-mobile-modules/src/data-access/graphql/product-details/queries.ts:26` ### Error Type Discrimination Handle different GraphQL error types appropriately: ```typescript export const handleGraphQLError = (error: ApolloError) => { // Authentication errors (401) const authError = error.graphQLErrors.find( (err) => err.extensions?.code === 'UNAUTHENTICATED' ); if (authError) { return { type: 'auth' as const, message: 'Please sign in again', action: 'reauth', }; } // Validation errors (400) const validationError = error.graphQLErrors.find( (err) => err.extensions?.code === 'BAD_USER_INPUT' ); if (validationError) { return { type: 'validation' as const, message: validationError.message, fields: validationError.extensions?.fields as Record, }; } // Network errors (no response) if (error.networkError) { return { type: 'network' as const, message: 'Connection failed. Please check your internet.', action: 'retry', }; } // Generic errors return { type: 'unknown' as const, message: 'Something went wrong. Please try again.', action: 'retry', }; }; ``` **Key error types:** - `UNAUTHENTICATED`: Session expired, need re-auth - `BAD_USER_INPUT`: Validation errors with field details - `networkError`: Connection failures - Generic: Unknown server errors **Why**: Specific error handling enables appropriate user feedback and recovery actions. ## Retry Strategies ### TanStack Query Retry Configuration Configure automatic retry for transient errors: ```typescript export const useLoadRecipes = () => { const { data, error, refetch, isRefetching } = useQuery({ queryKey: ['recipes'], queryFn: fetchRecipes, retry: 3, // Retry 3 times retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff }); return { recipes: data ?? [], error, refetch, isRefetching, }; }; ``` **Retry patterns:** - `retry: 3`: Retry up to 3 times - `retryDelay`: Exponential backoff (1s, 2s, 4s, max 30s) - Automatic for temporary network failures - No retry for 4xx errors (client errors) **Why**: Automatic retry handles temporary network issues without user intervention. Exponential backoff prevents overwhelming servers. ### Manual Retry Hook Provide manual retry for user-initiated actions: ```typescript export const useManualRetry = (onRetry: () => Promise) => { const [isRetrying, setIsRetrying] = useState(false); const [retryCount, setRetryCount] = useState(0); const retry = useCallback(async () => { setIsRetrying(true); setRetryCount((prev) => prev + 1); try { await onRetry(); } catch (error) { console.error('Retry failed:', error); } finally { setIsRetrying(false); } }, [onRetry]); return { retry, isRetrying, retryCount }; }; // Usage const ErrorScreen = ({ error, refetch }) => { const { retry, isRetrying } = useManualRetry(refetch); return ( Error: {error.message} ); }; ``` **Why**: Users can manually retry after fixing issues (e.g., enabling wifi). Visual loading state shows retry in progress. ## Error Boundary Lifecycle ### Lifecycle Callbacks Use lifecycle callbacks for custom error handling: ```typescript { // Component mounted successfully console.log('Checkout error boundary mounted'); }} beforeCapture={(scope, error, componentStack) => { // Before error is captured console.log('About to capture error in:', scope.moduleName); }} onError={(error, componentStack) => { // After error is captured logErrorToAnalytics(error, { moduleName: 'Checkout' }); }} onReset={(error, componentStack) => { // After error is reset (before app restart) console.log('Resetting error boundary'); }} onUnmount={(error, componentStack) => { // Component unmounting if (error) { console.log('Unmounting with active error'); } }} > ``` **Lifecycle order:** 1. `onMount()` - Component mounted 2. Error occurs in children 3. `beforeCapture()` - Before capturing error 4. Error state set 5. `onError()` - After error captured 6. User clicks retry 7. `onReset()` - Before app restart 8. `RNRestart.restart()` - App restarts **Why**: Lifecycle callbacks enable analytics tracking, custom logging, and cleanup before app restart. **Production Example**: `git-resources/shared-mobile-modules/src/libs/error-boundary/ErrorBoundary.tsx:60` ### Reset Error Strategy ErrorBoundary provides `resetError` that restarts the app: ```typescript const resetError = () => { // Reset error state this.setState({ error: null, componentStack: null, }); // Call onReset callback if (onReset) { onReset(error, componentStack); } // Reload JS Bundle RNRestart.restart(); }; ``` **Why**: `RNRestart.restart()` ensures a clean slate after errors. State is cleared and app reloads. **Production Example**: `git-resources/shared-mobile-modules/src/libs/error-boundary/ErrorBoundary.tsx:130` ## Testing Error Handling ### Test Error Boundaries Verify ErrorBoundary catches errors and shows fallback: ```typescript import { render, screen } from '@testing-library/react-native'; import { useEffect } from 'react'; const ThrowError = ({ shouldThrow = true }) => { useEffect(() => { if (shouldThrow) { throw new Error('Test error'); } }, [shouldThrow]); return null; }; describe('ErrorBoundary', () => { it('renders fallback when error occurs', () => { render( Error: {error.message}} > ); expect(screen.getByText('Error: Test error')).toBeTruthy(); }); it('calls onError callback', () => { const onError = jest.fn(); render( ); expect(onError).toHaveBeenCalledWith( expect.objectContaining({ message: 'Test error' }), expect.any(String) // componentStack ); }); }); ``` **Key patterns:** - Use `useEffect` to throw errors (componentDidCatch catches lifecycle errors) - Verify fallback UI is rendered - Verify `onError` callback is called with error and componentStack **Why**: Testing ensures error boundaries work correctly and capture all error details. **Production Example**: `git-resources/shared-mobile-modules/src/libs/error-boundary/ErrorBoundary.test.tsx:1` ### Test OpenTelemetry Spans Verify spans are created with correct attributes: ```typescript import { mockTracerProvider } from 'jest-utils'; describe('ErrorBoundary OTEL Tracing', () => { const mockSpanExporter = mockTracerProvider(); beforeEach(() => { mockSpanExporter.reset(); }); it('creates span when error occurs', () => { render( ); const spans = mockSpanExporter.getFinishedSpans(); expect(spans.length).toBe(1); const span = spans[0]; expect(span.name).toBe('ErrorBoundary TestModule'); expect(span.attributes).toEqual( expect.objectContaining({ 'module.name': 'TestModule', feature: 'checkout', userId: '123', componentStack: expect.any(String), }) ); }); }); ``` **Why**: Testing OTEL integration ensures errors are properly tracked in distributed tracing. **Production Example**: `git-resources/shared-mobile-modules/src/libs/error-boundary/ErrorBoundary.test.tsx:92` ### Test Retry Logic Verify retry behavior with mock failures: ```typescript test('retries failed queries with exponential backoff', async () => { const mockFetch = jest .fn() .mockRejectedValueOnce(new Error('Network error')) .mockRejectedValueOnce(new Error('Network error')) .mockResolvedValueOnce({ data: mockRecipes }); const { result } = renderHook(() => useQuery({ queryKey: ['recipes'], queryFn: mockFetch, retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }) ); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(mockFetch).toHaveBeenCalledTimes(3); // 2 failures + 1 success expect(result.current.data).toEqual({ data: mockRecipes }); }); ``` **Why**: Testing retry logic prevents infinite loops and validates backoff strategy. ## Common Mistakes to Avoid ❌ **Don't forget to end spans**: ```typescript // ❌ Missing span.end() - memory leak const span = tracer.startSpan('operation'); await doSomething(); // Forgot span.end() // ✅ Always end span in finally block const span = tracer.startSpan('operation'); try { await doSomething(); } catch (error) { span.recordException(error); throw error; } finally { span.end(); // CRITICAL } ``` ❌ **Don't show technical errors to users**: ```typescript // ❌ Technical stack trace confuses users {error.stack} // ✅ User-friendly message Something went wrong. Please try again. ``` ❌ **Don't swallow errors without logging**: ```typescript // ❌ Silent failure - no logging try { await fetchData(); } catch (error) { // Silent failure } // ✅ Log errors with context try { await fetchData(); } catch (error) { logError(error, { moduleName: 'DataFetcher', operation: 'fetchData' }); throw error; // Re-throw if caller needs to handle } ``` ❌ **Don't use errorPolicy: 'ignore' blindly**: ```typescript // ❌ Ignoring all errors silently useQuery(GetDataDocument, { errorPolicy: 'ignore', // Errors disappear }); // ✅ Use 'all' to get partial data + errors useQuery(GetDataDocument, { errorPolicy: 'all', // Partial data + errors }); ``` ✅ **Do provide scope context for errors**: ```typescript // ✅ Rich context for debugging ``` ✅ **Do use component-level boundaries**: ```typescript // ✅ Isolate errors per feature ``` ✅ **Do provide retry actions**: ```typescript // ✅ User can retry after fixing issue { refetch(); }} /> ``` ## Performance Considerations ### Avoid Creating Too Many Spans Don't create spans for every small operation: ```typescript // ❌ Too many spans - performance overhead items.forEach((item) => { const span = tracer.startSpan(`process-${item.id}`); processItem(item); span.end(); }); // ✅ Single span for batch operation const span = tracer.startSpan('process-items', { attributes: { 'items.count': items.length, }, }); try { items.forEach(processItem); span.setStatus({ code: SpanStatusCode.OK }); } finally { span.end(); } ``` **Why**: Too many spans impact performance and create noise in traces. Group related operations. ## Quick Reference **ErrorBoundary Pattern:** ```typescript ( )} onError={(error, componentStack) => { logError(error, { moduleName: 'FeatureName' }); }} > ``` **OpenTelemetry Span Pattern:** ```typescript const span = startSpan('operation.name', { attributes: { /* context */ } }); try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); throw error; } finally { span.end(); // ALWAYS } ``` **GraphQL Error Handling:** ```typescript useQuery(Document, { errorPolicy: 'all', // Partial data + errors }); ``` **Retry Configuration:** ```typescript useQuery({ queryKey: ['data'], queryFn: fetchData, retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }); ``` **Key Libraries:** - React Native 0.75.4 - @opentelemetry/api 2.0.1 - @apollo/client 3.13.6 - @tanstack/react-query 5.59.16 - react-native-restart (for ErrorBoundary reset) For production examples, see [references/examples.md](references/examples.md).