Error Tracking Guide
Complete guide for implementing error tracking and crash reporting in your app.
Table of Contents
- Overview
- Built-in Error Boundary
- Sentry Integration
- Bugsnag Integration
- Custom Error Logging
- 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
| Solution | Free Tier | Best For |
|---|---|---|
| Sentry | 5K errors/month | Most apps, great Expo support |
| Bugsnag | 7.5K errors/month | Enterprise, detailed sessions |
| Firebase Crashlytics | Unlimited | Firebase ecosystem users |
| Custom Logging | N/A | Simple 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
| Scenario | Solution |
|---|---|
| Small app, limited budget | Custom logging |
| Production app, need insights | Sentry |
| Enterprise, session replay | Bugsnag |
| Using Firebase already | Crashlytics |
| React Query errors | Built-in onError + Sentry |
| UI render errors | ErrorBoundary + logging |