Question #361MediumMonitoring & Analytics

How do you handle app crashes?

#crash-handling#firebase#crashlytics#error-handling

Answer

Overview

Handling app crashes is critical for maintaining a good user experience and debugging issues in production. Flutter provides several mechanisms to catch, log, and report crashes from both Dart code and native platform code.


1. Catching Dart Exceptions

FlutterError.onError

Catches errors that occur during the Flutter framework's execution (e.g., widget build errors).

dart
void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    // Log error details
    print('Flutter Error: ${details.exception}');
    print('Stack Trace: ${details.stack}');
    
    // Send to crash reporting service
    // FirebaseCrashlytics.instance.recordFlutterError(details);
  };

  runApp(MyApp());
}

2. Catching Async Exceptions

Use

text
runZonedGuarded
to catch uncaught async errors.

dart
import 'dart:async';

void main() {
  runZonedGuarded(
    () {
      // Setup Flutter error handler
      FlutterError.onError = (details) {
        print('Flutter Error: ${details.exception}');
        // Report to crash service
      };

      runApp(MyApp());
    },
    (error, stackTrace) {
      // Catches all uncaught async errors
      print('Uncaught Error: $error');
      print('Stack Trace: $stackTrace');
      // Report to crash service
    },
  );
}

3. Firebase Crashlytics Integration

Firebase Crashlytics is the most popular crash reporting solution for Flutter.

Setup

yaml
# pubspec.yaml
dependencies:
  firebase_core: ^2.24.0
  firebase_crashlytics: ^3.4.9

Implementation

dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'dart:async';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize Firebase
  await Firebase.initializeApp();

  // Pass all uncaught errors to Crashlytics
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  };

  // Pass all uncaught async errors to Crashlytics
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(MyApp());
}

Manual Error Logging

dart
try {
  await someRiskyOperation();
} catch (e, stackTrace) {
  // Log non-fatal error
  await FirebaseCrashlytics.instance.recordError(
    e,
    stackTrace,
    reason: 'Error during risky operation',
    fatal: false,
  );
}

Custom Keys and User Info

dart
// Set user identifier
FirebaseCrashlytics.instance.setUserIdentifier('user_12345');

// Add custom keys for debugging
FirebaseCrashlytics.instance.setCustomKey('page', 'checkout');
FirebaseCrashlytics.instance.setCustomKey('cart_value', 99.99);

// Log custom messages
FirebaseCrashlytics.instance.log('User clicked checkout button');

4. Sentry Integration

Sentry is another popular crash reporting tool.

yaml
# pubspec.yaml
dependencies:
  sentry_flutter: ^7.14.0
dart
import 'package:sentry_flutter/sentry_flutter.dart';

Future<void> main() async {
  await SentryFlutter.init(
    (options) {
      options.dsn = 'https://your-sentry-dsn.io/project-id
      options.tracesSampleRate = 1.0; // Performance monitoring
    },
    appRunner: () => runApp(MyApp()),
  );
}

// Manual error reporting
try {
  await riskyOperation();
} catch (e, stackTrace) {
  await Sentry.captureException(e, stackTrace: stackTrace);
}

5. Custom Error Screen

Show a friendly error screen instead of a red error screen in production.

dart
void main() {
  // In production, show custom error UI
  ErrorWidget.builder = (FlutterErrorDetails details) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.error_outline, size: 80, color: Colors.red),
              SizedBox(height: 16),
              Text(
                'Something went wrong!',
                style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 8),
              Text('Please restart the app.'),
            ],
          ),
        ),
      ),
    );
  };

  runApp(MyApp());
}

6. Platform-Specific Crash Handling

iOS (Native Crashes)

Firebase Crashlytics automatically handles native iOS crashes when configured.

Android (Native Crashes)

Firebase Crashlytics automatically handles native Android crashes when configured.

Configure in
text
AndroidManifest.xml

xml
<application>
  <meta-data
    android:name="firebase_crashlytics_collection_enabled"
    android:value="true" />
</application>

7. Testing Crash Reporting

Force a Crash for Testing

dart
// In development only
ElevatedButton(
  onPressed: () {
    // Force crash for testing
    FirebaseCrashlytics.instance.crash();
  },
  child: Text('Test Crash'),
)

// Throw test error
throw Exception('Test error for Crashlytics');

8. Best Practices

PracticeDescription
Catch all errorsUse
text
FlutterError.onError
and
text
runZonedGuarded
Log contextAdd custom keys (user ID, screen, action)
Test in debug modeTest crash reporting before production
Don't catch everythingLet fatal errors crash (easier to debug)
Monitor dashboardRegularly check Crashlytics/Sentry dashboard
Prioritize crashesFix high-frequency crashes first
Version trackingTag crashes with app version

9. Environment-Specific Handling

dart
void main() {
  const bool isProduction = bool.fromEnvironment('dart.vm.product');

  if (isProduction) {
    // Production: Send to crash service
    FlutterError.onError = (details) {
      FirebaseCrashlytics.instance.recordFlutterFatalError(details);
    };
  } else {
    // Development: Print to console
    FlutterError.onError = (details) {
      print('Flutter Error: ${details.exception}');
      print('Stack: ${details.stack}');
    };
  }

  runApp(MyApp());
}

10. Complete Example

dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // Flutter framework errors
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  };

  // Async errors
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  // Custom error widget
  ErrorWidget.builder = (details) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text('An error occurred. Please restart the app.'),
        ),
      ),
    );
  };

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Crash Handling Demo',
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Crash Handling')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                // This will be caught by FlutterError.onError
                throw Exception('Test Flutter Error');
              },
              child: Text('Throw Sync Error'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () async {
                // This will be caught by PlatformDispatcher.onError
                await Future.delayed(Duration(seconds: 1));
                throw Exception('Test Async Error');
              },
              child: Text('Throw Async Error'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                // Manual error logging
                try {
                  // Simulate error
                  int.parse('invalid');
                } catch (e, stack) {
                  FirebaseCrashlytics.instance.recordError(
                    e,
                    stack,
                    reason: 'Failed to parse number',
                  );
                }
              },
              child: Text('Log Manual Error'),
            ),
          ],
        ),
      ),
    );
  }
}

Summary

Handle crashes using FlutterError.onError for framework errors, PlatformDispatcher.onError for async errors, and integrate Firebase Crashlytics or Sentry for production monitoring. Always add context (user ID, custom keys) for easier debugging.

Learn more at Firebase Crashlytics and Sentry Flutter.