Error Tracking Guide

Complete guide for implementing error tracking and crash reporting in your app.


Table of Contents

  1. Overview
  2. Built-in Error Boundary
  3. Sentry Integration
  4. Bugsnag Integration
  5. Custom Error Logging
  6. Best Practices

Overview

Error tracking helps you:

  • Catch crashes before users report them
  • Debug production issues quickly
  • Monitor app stability
  • Prioritize fixes based on impact

Available Solutions

SolutionFree TierBest For
Sentry5K errors/monthMost apps, great Expo support
Bugsnag7.5K errors/monthEnterprise, detailed sessions
Firebase CrashlyticsUnlimitedFirebase ecosystem users
Custom LoggingN/ASimple needs, full control

Built-in Error Boundary

The boilerplate includes a React Error Boundary component.

Location

components/ErrorBoundary.tsx

Usage

import { ErrorBoundary } from '@/components/ErrorBoundary';

// Wrap components that might throw errors
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

// With custom fallback
<ErrorBoundary
  fallback={<CustomErrorScreen />}
  onError={(error, errorInfo) => {
    // Send to error tracking service
    logErrorToService(error, errorInfo);
  }}
>
  <MyComponent />
</ErrorBoundary>

ErrorFallback Component

import { ErrorFallback } from '@/components/ErrorBoundary';

// Use standalone for custom error states
<ErrorFallback
  error={error}
  title="Failed to load data"
  message="Please check your connection"
  onRetry={() => refetch()}
/>

Sentry Integration

1. Installation

npx expo install @sentry/react-native

2. Configuration

Add to app.json:

{
  "expo": {
    "plugins": [
      [
        "@sentry/react-native/expo",
        {
          "organization": "your-org",
          "project": "your-project"
        }
      ]
    ]
  }
}

3. Create Error Tracking Context

// contexts/ErrorTrackingContext.tsx
import React, { createContext, useContext, useEffect, ReactNode } from 'react';
import * as Sentry from '@sentry/react-native';
import { Platform } from 'react-native';

interface ErrorTrackingContextType {
  captureException: (error: Error, context?: Record<string, any>) => void;
  captureMessage: (message: string, level?: Sentry.SeverityLevel) => void;
  setUser: (user: { id: string; email?: string; username?: string } | null) => void;
  addBreadcrumb: (breadcrumb: Sentry.Breadcrumb) => void;
}

const ErrorTrackingContext = createContext<ErrorTrackingContextType | null>(null);

const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN;

export function ErrorTrackingProvider({ children }: { children: ReactNode }) {
  useEffect(() => {
    if (SENTRY_DSN && Platform.OS !== 'web') {
      Sentry.init({
        dsn: SENTRY_DSN,
        debug: __DEV__,
        environment: __DEV__ ? 'development' : 'production',
        enableAutoSessionTracking: true,
        sessionTrackingIntervalMillis: 30000,
        tracesSampleRate: __DEV__ ? 1.0 : 0.2,
        attachStacktrace: true,
        beforeSend(event) {
          // Filter out development errors
          if (__DEV__) {
            return null;
          }
          return event;
        },
      });
    }
  }, []);

  const captureException = (error: Error, context?: Record<string, any>) => {
    if (__DEV__) {
      console.error('Error captured:', error, context);
      return;
    }
    
    if (Platform.OS !== 'web') {
      Sentry.captureException(error, {
        extra: context,
      });
    }
  };

  const captureMessage = (message: string, level: Sentry.SeverityLevel = 'info') => {
    if (__DEV__) {
      console.log(`[${level}] ${message}`);
      return;
    }
    
    if (Platform.OS !== 'web') {
      Sentry.captureMessage(message, level);
    }
  };

  const setUser = (user: { id: string; email?: string; username?: string } | null) => {
    if (Platform.OS !== 'web') {
      Sentry.setUser(user);
    }
  };

  const addBreadcrumb = (breadcrumb: Sentry.Breadcrumb) => {
    if (Platform.OS !== 'web') {
      Sentry.addBreadcrumb(breadcrumb);
    }
  };

  return (
    <ErrorTrackingContext.Provider
      value={{
        captureException,
        captureMessage,
        setUser,
        addBreadcrumb,
      }}
    >
      {children}
    </ErrorTrackingContext.Provider>
  );
}

export function useErrorTracking() {
  const context = useContext(ErrorTrackingContext);
  if (!context) {
    throw new Error('useErrorTracking must be used within ErrorTrackingProvider');
  }
  return context;
}

4. Add to Root Layout

// app/_layout.tsx
import { ErrorTrackingProvider } from '@/contexts/ErrorTrackingContext';

export default function RootLayout() {
  return (
    <ErrorTrackingProvider>
      {/* ... other providers */}
      <RootLayoutNav />
    </ErrorTrackingProvider>
  );
}

5. Usage Examples

import { useErrorTracking } from '@/contexts/ErrorTrackingContext';

function MyComponent() {
  const { captureException, addBreadcrumb } = useErrorTracking();

  const handleAction = async () => {
    try {
      addBreadcrumb({
        category: 'action',
        message: 'User started payment',
        level: 'info',
      });
      
      await processPayment();
    } catch (error) {
      captureException(error as Error, {
        action: 'payment',
        amount: 9.99,
      });
    }
  };
}

6. Connect with Auth

// In your auth logic
const { setUser } = useErrorTracking();

// On login
setUser({
  id: user.id,
  email: user.email,
  username: user.name,
});

// On logout
setUser(null);

Bugsnag Integration

1. Installation

npx expo install @bugsnag/expo @bugsnag/plugin-expo-eas-sourcemaps

2. Configuration

Add to app.json:

{
  "expo": {
    "plugins": ["@bugsnag/plugin-expo-eas-sourcemaps"]
  }
}

3. Initialize

// utils/bugsnag.ts
import Bugsnag from '@bugsnag/expo';
import { Platform } from 'react-native';

const BUGSNAG_API_KEY = process.env.EXPO_PUBLIC_BUGSNAG_API_KEY;

export function initBugsnag() {
  if (BUGSNAG_API_KEY && Platform.OS !== 'web') {
    Bugsnag.start({
      apiKey: BUGSNAG_API_KEY,
      enabledReleaseStages: ['production', 'staging'],
      releaseStage: __DEV__ ? 'development' : 'production',
    });
  }
}

export function logError(error: Error, metadata?: Record<string, any>) {
  if (__DEV__) {
    console.error(error, metadata);
    return;
  }
  
  if (Platform.OS !== 'web') {
    Bugsnag.notify(error, (event) => {
      if (metadata) {
        event.addMetadata('custom', metadata);
      }
    });
  }
}

Custom Error Logging

For simple needs without external services:

Error Logger Utility

// utils/errorLogger.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

interface ErrorLog {
  id: string;
  timestamp: string;
  message: string;
  stack?: string;
  context?: Record<string, any>;
  deviceInfo: {
    platform: string;
    version: string;
  };
}

const ERROR_LOGS_KEY = '@error_logs';
const MAX_LOGS = 100;

export async function logError(
  error: Error,
  context?: Record<string, any>
): Promise<void> {
  try {
    const errorLog: ErrorLog = {
      id: Date.now().toString(),
      timestamp: new Date().toISOString(),
      message: error.message,
      stack: error.stack,
      context,
      deviceInfo: {
        platform: Platform.OS,
        version: Platform.Version.toString(),
      },
    };

    const existingLogs = await getErrorLogs();
    const updatedLogs = [errorLog, ...existingLogs].slice(0, MAX_LOGS);
    
    await AsyncStorage.setItem(ERROR_LOGS_KEY, JSON.stringify(updatedLogs));
    
    console.error('Error logged:', errorLog);
  } catch (e) {
    console.error('Failed to log error:', e);
  }
}

export async function getErrorLogs(): Promise<ErrorLog[]> {
  try {
    const logs = await AsyncStorage.getItem(ERROR_LOGS_KEY);
    return logs ? JSON.parse(logs) : [];
  } catch {
    return [];
  }
}

export async function clearErrorLogs(): Promise<void> {
  await AsyncStorage.removeItem(ERROR_LOGS_KEY);
}

export async function exportErrorLogs(): Promise<string> {
  const logs = await getErrorLogs();
  return JSON.stringify(logs, null, 2);
}

Integration with Error Boundary

// components/ErrorBoundary.tsx
import { logError } from '@/utils/errorLogger';

// In componentDidCatch:
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
  logError(error, { componentStack: errorInfo.componentStack });
  this.props.onError?.(error, errorInfo);
}

Debug Screen for Error Logs

// screens/DebugErrorsScreen.tsx
import { useState, useEffect } from 'react';
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
import { getErrorLogs, clearErrorLogs, exportErrorLogs } from '@/utils/errorLogger';
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';

export function DebugErrorsScreen() {
  const [logs, setLogs] = useState([]);

  useEffect(() => {
    loadLogs();
  }, []);

  const loadLogs = async () => {
    const errorLogs = await getErrorLogs();
    setLogs(errorLogs);
  };

  const handleExport = async () => {
    const data = await exportErrorLogs();
    const fileUri = FileSystem.documentDirectory + 'error-logs.json';
    await FileSystem.writeAsStringAsync(fileUri, data);
    await Sharing.shareAsync(fileUri);
  };

  const handleClear = async () => {
    await clearErrorLogs();
    setLogs([]);
  };

  return (
    <View style={{ flex: 1 }}>
      <View style={{ flexDirection: 'row', padding: 16 }}>
        <TouchableOpacity onPress={handleExport}>
          <Text>Export</Text>
        </TouchableOpacity>
        <TouchableOpacity onPress={handleClear}>
          <Text>Clear</Text>
        </TouchableOpacity>
      </View>
      <FlatList
        data={logs}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={{ padding: 16, borderBottomWidth: 1 }}>
            <Text style={{ fontWeight: 'bold' }}>{item.message}</Text>
            <Text>{item.timestamp}</Text>
          </View>
        )}
      />
    </View>
  );
}

Best Practices

1. Categorize Errors

// Use error types
class NetworkError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

// Capture with context
captureException(new NetworkError('API timeout'), {
  endpoint: '/api/users',
  timeout: 30000,
});

2. Add Breadcrumbs

// Track user journey
addBreadcrumb({ category: 'navigation', message: 'Opened profile' });
addBreadcrumb({ category: 'action', message: 'Tapped edit button' });
addBreadcrumb({ category: 'api', message: 'Fetching user data' });

// When error occurs, you'll see the trail

3. Don't Log Sensitive Data

// BAD - logging sensitive info
captureException(error, {
  password: user.password,
  creditCard: user.cardNumber,
});

// GOOD - log only identifiers
captureException(error, {
  userId: user.id,
  action: 'payment_failed',
});

4. Handle Different Error Types

const handleError = (error: unknown) => {
  if (error instanceof NetworkError) {
    // Show retry option
    showToast('Connection failed. Tap to retry.', 'error');
  } else if (error instanceof ValidationError) {
    // Show validation message
    showToast(error.message, 'warning');
  } else {
    // Unknown error - log and show generic message
    captureException(error as Error);
    showToast('Something went wrong', 'error');
  }
};

5. Set Up Alerts

Configure your error tracking service to alert you:

Sentry Alerts:
  - New issue detected
  - Issue regression
  - Error spike (>10 in 1 hour)
  
Bugsnag Alerts:
  - New error
  - Error threshold exceeded
  - Stability score drops

Environment Variables

# .env
EXPO_PUBLIC_SENTRY_DSN=https://xxx@sentry.io/xxx
EXPO_PUBLIC_BUGSNAG_API_KEY=your_api_key

Comparison: When to Use What

ScenarioSolution
Small app, limited budgetCustom logging
Production app, need insightsSentry
Enterprise, session replayBugsnag
Using Firebase alreadyCrashlytics
React Query errorsBuilt-in onError + Sentry
UI render errorsErrorBoundary + logging