Skip to content

Instantly share code, notes, and snippets.

@MonaAghili
Created May 4, 2025 08:22
Show Gist options
  • Save MonaAghili/232a1d2edf0c3e23b40a2c7369d78c03 to your computer and use it in GitHub Desktop.
Save MonaAghili/232a1d2edf0c3e23b40a2c7369d78c03 to your computer and use it in GitHub Desktop.
I created this boundery for my react native app is it a good solution or is there another solution? i use it as a provider for global error handeling can you refactor it?
import React, { createContext, ReactNode, useContext, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
const ErrorContext = createContext<ErrorContextType | undefined>(undefined);
interface ErrorContextType {
error: Error | null;
setError: (error: Error | null) => void;
resetError: () => void;
}
export function ErrorProvider({ children }: { children: ReactNode }) {
const [error, setError] = useState<Error | null>(null);
const resetError = () => setError(null);
const contextValue: ErrorContextType = {
error,
setError,
resetError,
};
// Render fallback UI or children
if (error) {
return (
<View style={styles.container}>
<Text style={styles.title}>Something went wrong</Text>
<Text style={styles.message}>{error.message || 'an unexpected error occurred'}</Text>
<TouchableOpacity style={styles.button} onPress={resetError}>
<Text style={styles.buttonText}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
return <ErrorContext.Provider value={contextValue}>{children}</ErrorContext.Provider>;
}
// Custom hook to use the error context
export function useError() {
const context = useContext(ErrorContext);
if (context === undefined) {
throw new Error('useError must be used within an ErrorProvider');
}
return context;
}
// a higher order component for try catch pattern
export function withErrorHandling<P extends object>(Component: React.ComponentType<P>) {
return function WithErrorHandling(props: P) {
const { setError } = useError();
try {
return <Component {...props} />;
} catch (error) {
setError(error instanceof Error ? error : new Error('An unknown error occurred'));
return null;
}
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
backgroundColor: '#fff',
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 12,
},
message: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 20,
},
button: {
backgroundColor: '#2196F3',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 4,
},
buttonText: {
color: 'white',
fontWeight: 'bold',
},
});
@shahryarjb
Copy link

Your error boundary implementation is a good starting point, but there's room for improvement in terms of flexibility, error categorization, and user experience. Let me refactor it to create a more robust global error handling solution for your React Native app.

Here's a refactored version with significant improvements:

import React, { createContext, ReactNode, useContext, useState, useEffect } from 'react';
import { 
  StyleSheet, 
  Text, 
  TouchableOpacity, 
  View, 
  SafeAreaView, 
  Platform,
  Dimensions,
  Image
} from 'react-native';

// Define error types for better categorization
export enum ErrorType {
  NETWORK = 'NETWORK',
  API = 'API',
  VALIDATION = 'VALIDATION',
  AUTHENTICATION = 'AUTHENTICATION',
  PERMISSION = 'PERMISSION',
  UNEXPECTED = 'UNEXPECTED'
}

// Enhanced error interface
export interface AppError extends Error {
  type: ErrorType;
  recoverable?: boolean;
  additionalInfo?: string;
  code?: string | number;
}

// Context type definition
interface ErrorContextType {
  error: AppError | null;
  setError: (error: AppError | null) => void;
  resetError: () => void;
  // Add a method to set errors with conveniences
  captureError: (
    originalError: any, 
    options?: { 
      type?: ErrorType, 
      recoverable?: boolean, 
      additionalInfo?: string,
      message?: string
    }
  ) => void;
}

// Create the context
const ErrorContext = createContext<ErrorContextType | undefined>(undefined);

interface ErrorProviderProps {
  children: ReactNode;
  // Allow customizing appearance
  customStyles?: Partial<typeof styles>;
  // Allow swapping out the error component entirely
  ErrorComponent?: React.ComponentType<{error: AppError, resetError: () => void}>;
}

export function ErrorProvider({ 
  children, 
  customStyles = {}, 
  ErrorComponent 
}: ErrorProviderProps) {
  const [error, setError] = useState<AppError | null>(null);
  const [isAutoResetting, setIsAutoResetting] = useState(false);

  // Helper function to create standardized errors
  const captureError = (
    originalError: any,
    options: { 
      type?: ErrorType, 
      recoverable?: boolean, 
      additionalInfo?: string,
      message?: string
    } = {}
  ) => {
    // If it's already an AppError, just use it
    if (originalError && 'type' in originalError) {
      setError(originalError as AppError);
      return;
    }

    // Create a new AppError
    const appError = new Error(
      options.message || 
      (originalError?.message || 'An unexpected error occurred')
    ) as AppError;
    
    appError.type = options.type || ErrorType.UNEXPECTED;
    appError.recoverable = options.recoverable ?? true;
    appError.additionalInfo = options.additionalInfo;
    appError.stack = originalError?.stack;
    appError.code = originalError?.code;
    
    setError(appError);
  };

  const resetError = () => {
    setError(null);
    setIsAutoResetting(false);
  };

  // Auto-reset for network errors after some time
  useEffect(() => {
    if (error?.type === ErrorType.NETWORK && !isAutoResetting) {
      setIsAutoResetting(true);
      const timer = setTimeout(() => {
        resetError();
      }, 30000); // 30 seconds
      
      return () => clearTimeout(timer);
    }
  }, [error, isAutoResetting]);

  const contextValue: ErrorContextType = {
    error,
    setError,
    resetError,
    captureError
  };

  // Render fallback UI or children
  if (error) {
    // Use custom component if provided
    if (ErrorComponent) {
      return <ErrorComponent error={error} resetError={resetError} />;
    }

    // Default error UI
    return <DefaultErrorComponent error={error} resetError={resetError} customStyles={customStyles} />;
  }

  return <ErrorContext.Provider value={contextValue}>{children}</ErrorContext.Provider>;
}

// Default error component
function DefaultErrorComponent({ 
  error, 
  resetError,
  customStyles = {}
}: { 
  error: AppError; 
  resetError: () => void;
  customStyles?: Partial<typeof styles>;
}) {
  // Merge custom styles with defaults
  const mergedStyles = StyleSheet.create({
    ...styles,
    ...customStyles,
  });

  // Error-specific UI logic
  const getErrorDetails = () => {
    switch (error.type) {
      case ErrorType.NETWORK:
        return {
          title: 'Network Error',
          message: error.message || 'Unable to connect. Please check your internet connection.',
          icon: require('../assets/network-error.png'), // You'll need to add these images
          buttonText: 'Try Again',
          buttonStyle: mergedStyles.primaryButton
        };
      case ErrorType.API:
        return {
          title: 'Service Unavailable',
          message: error.message || 'The server is currently unavailable. Please try again later.',
          icon: require('../assets/server-error.png'),
          buttonText: 'Try Again',
          buttonStyle: mergedStyles.primaryButton
        };
      case ErrorType.AUTHENTICATION:
        return {
          title: 'Authentication Error',
          message: error.message || 'Your session has expired. Please log in again.',
          icon: require('../assets/auth-error.png'),
          buttonText: 'Log In',
          buttonStyle: mergedStyles.warningButton
        };
      default:
        return {
          title: 'Something Went Wrong',
          message: error.message || 'An unexpected error occurred.',
          icon: require('../assets/general-error.png'),
          buttonText: 'Try Again',
          buttonStyle: mergedStyles.primaryButton
        };
    }
  };

  const errorDetails = getErrorDetails();

  return (
    <SafeAreaView style={mergedStyles.container}>
      <View style={mergedStyles.contentContainer}>
        {errorDetails.icon && (
          <Image 
            source={errorDetails.icon} 
            style={mergedStyles.errorIcon} 
            resizeMode="contain"
          />
        )}
        
        <Text style={mergedStyles.title}>{errorDetails.title}</Text>
        
        <Text style={mergedStyles.message}>{errorDetails.message}</Text>
        
        {error.additionalInfo && (
          <Text style={mergedStyles.additionalInfo}>{error.additionalInfo}</Text>
        )}
        
        {error.recoverable !== false && (
          <TouchableOpacity 
            style={[mergedStyles.button, errorDetails.buttonStyle]} 
            onPress={resetError}
          >
            <Text style={mergedStyles.buttonText}>{errorDetails.buttonText}</Text>
          </TouchableOpacity>
        )}
      </View>
    </SafeAreaView>
  );
}

// Custom hook to use the error context
export function useError() {
  const context = useContext(ErrorContext);

  if (context === undefined) {
    throw new Error('useError must be used within an ErrorProvider');
  }

  return context;
}

// Enhanced HOC for try/catch pattern with more options
export function withErrorHandling<P extends object>(
  Component: React.ComponentType<P>,
  options: {
    // Default error type if not specified
    defaultErrorType?: ErrorType;
    // Should return null on error or render a fallback component
    fallback?: React.ReactNode | ((error: AppError) => React.ReactNode);
  } = {}
) {
  return function WithErrorHandling(props: P) {
    const { captureError } = useError();
    const [localError, setLocalError] = useState<AppError | null>(null);

    // Reset local error when component remounts
    useEffect(() => {
      return () => setLocalError(null);
    }, []);

    try {
      if (localError) {
        // Render fallback if provided
        if (options.fallback) {
          return typeof options.fallback === 'function' 
            ? options.fallback(localError) 
            : options.fallback;
        }
        return null;
      }

      return <Component {...props} />;
    } catch (error) {
      // Only capture if not already captured
      if (!localError) {
        const appError = error as AppError;
        setLocalError(appError);
        
        captureError(error, {
          type: appError.type || options.defaultErrorType || ErrorType.UNEXPECTED
        });
      }
      
      // Render fallback if provided
      if (options.fallback) {
        return typeof options.fallback === 'function' && localError
          ? options.fallback(localError) 
          : options.fallback;
      }
      
      return null;
    }
  };
}

// Utility to create error boundaries for specific sections
export function ErrorBoundary({ 
  children, 
  fallback,
  onError 
}: { 
  children: ReactNode;
  fallback?: React.ReactNode | ((error: AppError, resetError: () => void) => React.ReactNode);
  onError?: (error: AppError) => void;
}) {
  const [localError, setLocalError] = useState<AppError | null>(null);
  const { captureError } = useError();

  const resetLocalError = () => setLocalError(null);

  // Use React's error boundary pattern
  useEffect(() => {
    if (localError) {
      // Report to global error system if requested
      if (onError) {
        onError(localError);
      }
    }
  }, [localError, onError]);

  // Custom error handler for this boundary
  const handleError = (error: any) => {
    const appError = error instanceof Error
      ? { ...error, type: ErrorType.UNEXPECTED } as AppError
      : new Error('An unknown error occurred') as AppError;
    
    appError.type = appError.type || ErrorType.UNEXPECTED;
    setLocalError(appError);
    captureError(appError);
  };

  if (localError) {
    if (fallback) {
      return typeof fallback === 'function'
        ? fallback(localError, resetLocalError)
        : fallback;
    }
    
    return (
      <View style={styles.localErrorContainer}>
        <Text style={styles.localErrorText}>{localError.message}</Text>
        <TouchableOpacity style={styles.localErrorButton} onPress={resetLocalError}>
          <Text style={styles.buttonText}>Retry</Text>
        </TouchableOpacity>
      </View>
    );
  }

  try {
    return <>{children}</>;
  } catch (error) {
    handleError(error);
    return null;
  }
}

// Better styled default styles
const { width, height } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  contentContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 24,
  },
  errorIcon: {
    width: width * 0.4,
    height: width * 0.4,
    marginBottom: 24,
  },
  title: {
    fontSize: 22,
    fontWeight: 'bold',
    marginBottom: 12,
    textAlign: 'center',
    color: '#222',
  },
  message: {
    fontSize: 16,
    color: '#555',
    textAlign: 'center',
    marginBottom: 16,
    lineHeight: 22,
  },
  additionalInfo: {
    fontSize: 14,
    color: '#777',
    textAlign: 'center',
    marginBottom: 24,
    fontStyle: 'italic',
  },
  button: {
    paddingVertical: 12,
    paddingHorizontal: 32,
    borderRadius: 8,
    minWidth: width * 0.5,
    alignItems: 'center',
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 4,
      },
      android: {
        elevation: 3,
      },
    }),
  },
  primaryButton: {
    backgroundColor: '#2196F3',
  },
  warningButton: {
    backgroundColor: '#FF9800',
  },
  dangerButton: {
    backgroundColor: '#F44336',
  },
  buttonText: {
    color: 'white',
    fontWeight: '600',
    fontSize: 16,
  },
  // Styles for local error boundaries
  localErrorContainer: {
    padding: 16,
    backgroundColor: '#FFF8E1',
    borderRadius: 8,
    margin: 8,
    borderWidth: 1,
    borderColor: '#FFE082',
    alignItems: 'center',
  },
  localErrorText: {
    color: '#5D4037',
    marginBottom: 12,
    textAlign: 'center',
  },
  localErrorButton: {
    backgroundColor: '#FFA000',
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 4,
  },
});

Key Improvements in the Refactored Error Boundary

  1. Error Type Classification:

    • Added an ErrorType enum to categorize errors (network, API, validation, etc.)
    • More specific error handling based on error types
  2. Enhanced Error Interface:

    • Created an AppError interface that extends the standard Error
    • Added properties like recoverable, additionalInfo, and code
  3. User-Friendly Error Messages:

    • Different error displays based on error type
    • Support for custom icons and styles per error type
  4. Auto-Recovery:

    • Auto-reset for network errors after a timeout
    • Different recovery options based on error type
  5. Customization Options:

    • Allow passing custom styles
    • Support for custom error components
    • Configurable fallback rendering
  6. Local Error Boundaries:

    • Added ErrorBoundary component for wrapping specific parts of your app
    • Different from the global provider for more targeted error handling
  7. Improved Error Capturing:

    • Added a captureError helper for easier error creation
    • Better type safety and error standardization
  8. Better Visual Design:

    • SafeAreaView for proper insets
    • Responsive sizing using screen dimensions
    • Platform-specific shadows
    • Custom button styles based on error severity

Example Usage

// App.tsx
import { ErrorProvider, ErrorType } from './components/error/ErrorProvider';

export default function App() {
  return (
    <ErrorProvider>
      <NavigationContainer>
        <Stack />
      </NavigationContainer>
    </ErrorProvider>
  );
}

// SomeScreen.tsx
import { useError, ErrorType, ErrorBoundary } from './components/error/ErrorProvider';

function SomeScreen() {
  const { captureError } = useError();
  
  const handleApiCall = async () => {
    try {
      const response = await fetchData();
      // Process data
    } catch (error) {
      captureError(error, { 
        type: ErrorType.API,
        message: 'Failed to load data',
        additionalInfo: 'Please try again later or contact support.'
      });
    }
  };
  
  return (
    <View>
      <Text>Some content</Text>
      
      {/* Local error boundary for just this section */}
      <ErrorBoundary 
        fallback={(error, reset) => (
          <TouchableOpacity onPress={reset}>
            <Text>This section failed: {error.message}</Text>
          </TouchableOpacity>
        )}
      >
        <RiskyComponent />
      </ErrorBoundary>
      
      <Button title="Fetch Data" onPress={handleApiCall} />
    </View>
  );
}

// Using the HOC
const ProtectedComponent = withErrorHandling(RiskyComponent, {
  defaultErrorType: ErrorType.UNEXPECTED,
  fallback: <Text>Something went wrong in this component</Text>
});

This refactored error boundary system provides a much more flexible and user-friendly approach to error handling in your React Native app. It allows for global error handling while also supporting localized error boundaries, customizable UI, and intelligent error recovery based on error types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment