@buivietphi/skill-mobile-mt 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,394 @@
1
+ # Error Handling — Resilient Mobile Code
2
+
3
+ > On-demand module. Loaded when implementing error handling, retry strategies, error boundaries, or user-facing error messages.
4
+ > Contains patterns that prevent crashes and provide graceful degradation.
5
+
6
+ ---
7
+
8
+ ## Error Type Hierarchy
9
+
10
+ ### React Native / TypeScript
11
+
12
+ ```typescript
13
+ // errors/AppError.ts
14
+
15
+ export class AppError extends Error {
16
+ readonly code: string;
17
+ readonly isRetryable: boolean;
18
+ readonly statusCode?: number;
19
+
20
+ constructor(message: string, code: string, isRetryable: boolean, statusCode?: number) {
21
+ super(message);
22
+ this.name = 'AppError';
23
+ this.code = code;
24
+ this.isRetryable = isRetryable;
25
+ this.statusCode = statusCode;
26
+ }
27
+ }
28
+
29
+ export class NetworkError extends AppError {
30
+ constructor(message = 'Network connection failed') {
31
+ super(message, 'NETWORK_ERROR', true);
32
+ }
33
+ }
34
+
35
+ export class TimeoutError extends AppError {
36
+ constructor(message = 'Request timed out') {
37
+ super(message, 'TIMEOUT', true);
38
+ }
39
+ }
40
+
41
+ export class AuthError extends AppError {
42
+ constructor(message = 'Authentication failed', code = 'AUTH_ERROR') {
43
+ super(message, code, false, 401);
44
+ }
45
+ }
46
+
47
+ export class ForbiddenError extends AppError {
48
+ constructor(message = 'Access denied') {
49
+ super(message, 'FORBIDDEN', false, 403);
50
+ }
51
+ }
52
+
53
+ export class ValidationError extends AppError {
54
+ readonly fieldErrors: Record<string, string[]>;
55
+
56
+ constructor(message: string, fieldErrors: Record<string, string[]> = {}) {
57
+ super(message, 'VALIDATION', false, 422);
58
+ this.fieldErrors = fieldErrors;
59
+ }
60
+ }
61
+
62
+ export class ServerError extends AppError {
63
+ constructor(message = 'Server error', statusCode = 500) {
64
+ super(message, 'SERVER_ERROR', true, statusCode);
65
+ }
66
+ }
67
+
68
+ export class NotFoundError extends AppError {
69
+ constructor(message = 'Not found') {
70
+ super(message, 'NOT_FOUND', false, 404);
71
+ }
72
+ }
73
+
74
+ export class RateLimitError extends AppError {
75
+ readonly retryAfter: number; // seconds
76
+
77
+ constructor(retryAfter = 60) {
78
+ super('Too many requests', 'RATE_LIMIT', true, 429);
79
+ this.retryAfter = retryAfter;
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Error Normalizer (from API responses)
85
+
86
+ ```typescript
87
+ // errors/normalizeError.ts
88
+ import { AxiosError } from 'axios';
89
+
90
+ export function normalizeError(error: unknown): AppError {
91
+ if (error instanceof AppError) return error;
92
+
93
+ if (error instanceof AxiosError) {
94
+ // No response — network error
95
+ if (!error.response) {
96
+ if (error.code === 'ECONNABORTED') return new TimeoutError();
97
+ return new NetworkError();
98
+ }
99
+
100
+ const { status, data } = error.response;
101
+
102
+ switch (status) {
103
+ case 401: return new AuthError(data?.message);
104
+ case 403: return new ForbiddenError(data?.message);
105
+ case 404: return new NotFoundError(data?.message);
106
+ case 422: return new ValidationError(data?.message, data?.errors);
107
+ case 429: return new RateLimitError(parseInt(error.response.headers['retry-after'] || '60'));
108
+ default:
109
+ if (status >= 500) return new ServerError(data?.message, status);
110
+ return new AppError(data?.message || 'Unknown error', 'UNKNOWN', false, status);
111
+ }
112
+ }
113
+
114
+ if (error instanceof Error) {
115
+ return new AppError(error.message, 'UNKNOWN', false);
116
+ }
117
+
118
+ return new AppError('An unexpected error occurred', 'UNKNOWN', false);
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## User-Facing Error Messages
125
+
126
+ ### Error Message Mapper
127
+
128
+ ```typescript
129
+ // errors/errorMessages.ts
130
+
131
+ const userMessages: Record<string, string> = {
132
+ NETWORK_ERROR: 'No internet connection. Please check your network and try again.',
133
+ TIMEOUT: 'The request is taking too long. Please try again.',
134
+ AUTH_ERROR: 'Your session has expired. Please log in again.',
135
+ FORBIDDEN: "You don't have permission to do this.",
136
+ NOT_FOUND: 'The item you are looking for no longer exists.',
137
+ VALIDATION: 'Please check your input and try again.',
138
+ SERVER_ERROR: 'Something went wrong on our end. Please try again later.',
139
+ RATE_LIMIT: 'Too many requests. Please wait a moment and try again.',
140
+ UNKNOWN: 'Something unexpected happened. Please try again.',
141
+ };
142
+
143
+ export function getUserMessage(error: AppError): string {
144
+ return userMessages[error.code] ?? userMessages.UNKNOWN;
145
+ }
146
+
147
+ export function getRetryLabel(error: AppError): string | null {
148
+ if (!error.isRetryable) return null;
149
+ if (error instanceof RateLimitError) return `Retry in ${error.retryAfter}s`;
150
+ return 'Try Again';
151
+ }
152
+ ```
153
+
154
+ ### Error Display Component
155
+
156
+ ```typescript
157
+ // components/ErrorView.tsx
158
+ interface Props {
159
+ error: AppError | Error;
160
+ onRetry?: () => void;
161
+ }
162
+
163
+ export function ErrorView({ error, onRetry }: Props) {
164
+ const appError = error instanceof AppError ? error : normalizeError(error);
165
+ const message = getUserMessage(appError);
166
+ const retryLabel = getRetryLabel(appError);
167
+
168
+ return (
169
+ <View style={styles.container}>
170
+ <ErrorIcon size={48} color={colors.error} />
171
+ <Text style={styles.message}>{message}</Text>
172
+ {retryLabel && onRetry && (
173
+ <Button title={retryLabel} onPress={onRetry} />
174
+ )}
175
+ </View>
176
+ );
177
+ }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Global Error Handling
183
+
184
+ ### React Native — Error Boundary
185
+
186
+ ```typescript
187
+ // components/ErrorBoundary.tsx
188
+ import React from 'react';
189
+
190
+ interface State {
191
+ hasError: boolean;
192
+ error: Error | null;
193
+ }
194
+
195
+ export class ErrorBoundary extends React.Component<
196
+ { children: React.ReactNode; fallback?: React.ReactNode },
197
+ State
198
+ > {
199
+ state: State = { hasError: false, error: null };
200
+
201
+ static getDerivedStateFromError(error: Error): State {
202
+ return { hasError: true, error };
203
+ }
204
+
205
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
206
+ // Log to crash reporting service
207
+ crashReporting.recordError(error, { componentStack: info.componentStack });
208
+ }
209
+
210
+ resetError = () => {
211
+ this.setState({ hasError: false, error: null });
212
+ };
213
+
214
+ render() {
215
+ if (this.state.hasError) {
216
+ return this.props.fallback ?? (
217
+ <View style={styles.center}>
218
+ <Text>Something went wrong</Text>
219
+ <Button title="Try Again" onPress={this.resetError} />
220
+ </View>
221
+ );
222
+ }
223
+ return this.props.children;
224
+ }
225
+ }
226
+
227
+ // Wrap at app root:
228
+ // <ErrorBoundary>
229
+ // <App />
230
+ // </ErrorBoundary>
231
+ ```
232
+
233
+ ### Unhandled Promise Rejection Handler
234
+
235
+ ```typescript
236
+ // app/_layout.tsx or App.tsx — setup once at root
237
+ import { LogBox } from 'react-native';
238
+
239
+ // Capture unhandled promise rejections
240
+ if (__DEV__) {
241
+ // Show in dev console
242
+ LogBox.ignoreLogs(['Require cycle:']);
243
+ } else {
244
+ // In production: log to crash service, don't crash the app
245
+ const originalHandler = ErrorUtils.getGlobalHandler();
246
+ ErrorUtils.setGlobalHandler((error, isFatal) => {
247
+ crashReporting.recordError(error, { isFatal });
248
+ if (!isFatal) return; // non-fatal: swallow
249
+ originalHandler(error, isFatal); // fatal: let RN handle
250
+ });
251
+ }
252
+ ```
253
+
254
+ ### Flutter — Global Error Handler
255
+
256
+ ```dart
257
+ // main.dart
258
+ void main() {
259
+ FlutterError.onError = (details) {
260
+ FlutterError.presentError(details);
261
+ // Log to Crashlytics/Sentry
262
+ crashReporting.recordFlutterError(details);
263
+ };
264
+
265
+ PlatformDispatcher.instance.onError = (error, stack) {
266
+ crashReporting.recordError(error, stack);
267
+ return true; // handled
268
+ };
269
+
270
+ runApp(const MyApp());
271
+ }
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Retry Strategies
277
+
278
+ ### Exponential Backoff with Jitter
279
+
280
+ ```typescript
281
+ // utils/retry.ts
282
+ interface RetryOptions {
283
+ maxRetries?: number;
284
+ baseDelay?: number;
285
+ maxDelay?: number;
286
+ shouldRetry?: (error: AppError, attempt: number) => boolean;
287
+ onRetry?: (attempt: number, delay: number) => void;
288
+ }
289
+
290
+ export async function withRetry<T>(
291
+ fn: () => Promise<T>,
292
+ options: RetryOptions = {}
293
+ ): Promise<T> {
294
+ const {
295
+ maxRetries = 3,
296
+ baseDelay = 1000,
297
+ maxDelay = 30000,
298
+ shouldRetry = (error) => error.isRetryable,
299
+ onRetry,
300
+ } = options;
301
+
302
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
303
+ try {
304
+ return await fn();
305
+ } catch (error) {
306
+ const appError = normalizeError(error);
307
+
308
+ if (attempt === maxRetries || !shouldRetry(appError, attempt)) {
309
+ throw appError;
310
+ }
311
+
312
+ // Exponential backoff: 1s, 2s, 4s + random jitter
313
+ const delay = Math.min(
314
+ baseDelay * Math.pow(2, attempt) + Math.random() * 500,
315
+ maxDelay
316
+ );
317
+
318
+ onRetry?.(attempt + 1, delay);
319
+ await new Promise(r => setTimeout(r, delay));
320
+ }
321
+ }
322
+
323
+ throw new AppError('Max retries exceeded', 'MAX_RETRIES', false);
324
+ }
325
+
326
+ // Usage:
327
+ // const data = await withRetry(() => api.get('/products'), {
328
+ // maxRetries: 3,
329
+ // onRetry: (attempt) => console.log(`Retry #${attempt}`),
330
+ // });
331
+ ```
332
+
333
+ ### Rate Limit Handling
334
+
335
+ ```typescript
336
+ // Automatic rate limit handling in API interceptor
337
+ api.interceptors.response.use(
338
+ (response) => response,
339
+ async (error: AxiosError) => {
340
+ if (error.response?.status === 429) {
341
+ const retryAfter = parseInt(error.response.headers['retry-after'] || '5');
342
+ await new Promise(r => setTimeout(r, retryAfter * 1000));
343
+ return api(error.config!);
344
+ }
345
+ return Promise.reject(error);
346
+ }
347
+ );
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Toast / Snackbar Error Notifications
353
+
354
+ ```typescript
355
+ // hooks/useToast.ts
356
+ import { useCallback } from 'react';
357
+
358
+ type ToastType = 'success' | 'error' | 'warning' | 'info';
359
+
360
+ // Simple toast context (or use a library like react-native-toast-message)
361
+ export function useToast() {
362
+ const show = useCallback((message: string, type: ToastType = 'info') => {
363
+ // Use your preferred toast library
364
+ Toast.show({ type, text1: message, visibilityTime: 4000 });
365
+ }, []);
366
+
367
+ const showError = useCallback((error: unknown) => {
368
+ const appError = normalizeError(error);
369
+ show(getUserMessage(appError), 'error');
370
+ }, [show]);
371
+
372
+ return { show, showError };
373
+ }
374
+
375
+ // Usage in mutation:
376
+ // const toast = useToast();
377
+ // try { await addToCart(product.id); toast.show('Added to cart', 'success'); }
378
+ // catch (e) { toast.showError(e); }
379
+ ```
380
+
381
+ ---
382
+
383
+ ## Error Recovery Patterns
384
+
385
+ ```
386
+ NETWORK ERROR → Show offline banner + retry button + queue mutations
387
+ AUTH ERROR (401) → Auto-refresh token → if fail → redirect to login
388
+ FORBIDDEN (403) → Show "Access denied" + navigate back
389
+ NOT FOUND (404) → Show "Not found" + navigate back
390
+ VALIDATION (422) → Highlight form fields with errors
391
+ SERVER ERROR (5xx) → Show retry button + log to crash service
392
+ RATE LIMIT (429) → Show countdown timer + auto-retry after delay
393
+ TIMEOUT → Retry with longer timeout (max 3 attempts)
394
+ ```