Error Handling & API Abstraction Architecture

0/3 in this phase0/35 across the roadmap

📖 Concept

Error handling architecture in React Native must cover multiple failure domains:

  1. Network errors — No connectivity, timeouts, 5xx errors
  2. API errors — 4xx responses, validation errors, auth failures
  3. Runtime errors — JS exceptions, native crashes, unhandled rejections
  4. State errors — Corrupted state, impossible state transitions
  5. Navigation errors — Invalid routes, deep link failures

Principles of production error handling:

  • Errors are data, not exceptions — Model errors as part of your state (discriminated unions)
  • Fail gracefully, not silently — Always inform the user AND log for debugging
  • Centralize error reporting — One error reporting pipeline (Sentry, Crashlytics)
  • Isolate failures — Error boundary per feature so one crash doesn't take down the app
  • Retry with backoff — Network failures should auto-retry with exponential backoff

API abstraction layer: Don't scatter fetch() calls throughout your components. Build a centralized API client that handles:

  • Authentication (token injection, refresh)
  • Error normalization (different APIs return errors differently)
  • Request/response interceptors (logging, analytics)
  • Retry logic (with backoff)
  • Offline queuing
  • Response caching
  • Request deduplication

💻 Code Example

codeTap to expand ⛶
1// === PRODUCTION ERROR HANDLING ARCHITECTURE ===
2
3// 1. Error type hierarchy
4class AppError extends Error {
5 constructor(
6 message: string,
7 public code: string,
8 public severity: 'critical' | 'error' | 'warning' | 'info',
9 public userMessage?: string,
10 public metadata?: Record<string, any>,
11 ) {
12 super(message);
13 this.name = 'AppError';
14 }
15}
16
17class NetworkError extends AppError {
18 constructor(
19 message: string,
20 public statusCode?: number,
21 public retryable: boolean = true,
22 ) {
23 super(message, 'NETWORK_ERROR', statusCode && statusCode >= 500 ? 'critical' : 'error');
24 }
25}
26
27class AuthError extends AppError {
28 constructor(message: string) {
29 super(message, 'AUTH_ERROR', 'error', 'Please sign in again');
30 }
31}
32
33class ValidationError extends AppError {
34 constructor(
35 message: string,
36 public fields: Record<string, string[]>,
37 ) {
38 super(message, 'VALIDATION_ERROR', 'warning');
39 }
40}
41
42// 2. Central error reporter
43class ErrorReporter {
44 private static instance: ErrorReporter;
45
46 static getInstance(): ErrorReporter {
47 if (!ErrorReporter.instance) {
48 ErrorReporter.instance = new ErrorReporter();
49 }
50 return ErrorReporter.instance;
51 }
52
53 report(error: Error, context?: Record<string, any>) {
54 // Always log locally
55 console.error('[ErrorReporter]', error.message, context);
56
57 if (error instanceof AppError) {
58 // Structured error — send with metadata
59 Sentry.captureException(error, {
60 tags: { errorCode: error.code, severity: error.severity },
61 extra: { ...error.metadata, ...context },
62 });
63
64 // Critical errors alert on-call
65 if (error.severity === 'critical') {
66 this.alertOnCall(error);
67 }
68 } else {
69 // Unexpected error — always critical
70 Sentry.captureException(error, { extra: context });
71 }
72 }
73
74 private alertOnCall(error: AppError) {
75 // PagerDuty, Opsgenie, etc.
76 }
77}
78
79// 3. API Client with interceptors and retry
80class APIClient {
81 private interceptors: RequestInterceptor[] = [];
82
83 constructor(
84 private config: APIConfig,
85 private errorReporter: ErrorReporter,
86 ) {
87 // Default interceptors
88 this.addInterceptor(new AuthInterceptor());
89 this.addInterceptor(new LoggingInterceptor());
90 }
91
92 async request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
93 const { retries = 3, retryDelay = 1000, ...fetchOptions } = options;
94
95 // Apply interceptors
96 let config = { url: `${this.config.baseURL}${endpoint}`, ...fetchOptions };
97 for (const interceptor of this.interceptors) {
98 config = await interceptor.onRequest(config);
99 }
100
101 // Retry with exponential backoff
102 for (let attempt = 0; attempt <= retries; attempt++) {
103 try {
104 const response = await fetch(config.url, config);
105
106 if (response.status === 401) {
107 throw new AuthError('Token expired');
108 }
109
110 if (!response.ok) {
111 const body = await response.json().catch(() => ({}));
112 throw new NetworkError(
113 body.message || `HTTP ${response.status}`,
114 response.status,
115 response.status >= 500, // 5xx = retryable
116 );
117 }
118
119 return response.json();
120 } catch (error) {
121 if (error instanceof AuthError) {
122 // Try token refresh once
123 const refreshed = await this.refreshToken();
124 if (refreshed) continue; // Retry with new token
125 throw error; // Refresh failed
126 }
127
128 if (error instanceof NetworkError && error.retryable && attempt < retries) {
129 // Exponential backoff: 1s, 2s, 4s
130 await this.delay(retryDelay * Math.pow(2, attempt));
131 continue;
132 }
133
134 this.errorReporter.report(error as Error, { endpoint, attempt });
135 throw error;
136 }
137 }
138
139 throw new NetworkError('Max retries exceeded', undefined, false);
140 }
141
142 private delay(ms: number): Promise<void> {
143 return new Promise(resolve => setTimeout(resolve, ms));
144 }
145}

🏋️ Practice Exercise

Error Handling Exercises:

  1. Implement an API client with retry logic and exponential backoff
  2. Create a global error boundary that catches rendering errors and reports to Sentry
  3. Build a network status monitor that shows an offline banner and queues failed requests
  4. Implement token refresh logic that transparently retries the failed request
  5. Create error types for all failure modes in your app and map them to user-facing messages
  6. Build a debug screen that shows recent errors, network requests, and state snapshots

⚠️ Common Mistakes

  • Catching errors silently — catch (e) {} hides bugs; always log or report errors even if you handle them gracefully

  • Showing raw error messages to users — 'TypeError: Cannot read property x of undefined' is not a user message

  • Not implementing retry logic for transient network failures — user gets an error on a flaky connection instead of automatic recovery

  • Not isolating failures with error boundaries — one component's render error crashes the entire app

  • Inconsistent error handling — some API calls have try/catch, others don't; some show alerts, others show inline errors

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Error Handling & API Abstraction Architecture. Login to unlock this feature.