error_boundary
A robust error boundary widget for Flutter that catches and handles errors gracefully, preventing app crashes and providing fallback UI.
Inspired by React's Error Boundaries, this package brings the same pattern to Flutter with additional features like recovery strategies, async error catching, and pluggable error reporters.
Features
- Catch build errors - Prevent widget build failures from crashing your app
- Catch async errors - Optionally catch errors from Futures and Streams within the boundary
- Custom fallback UI - Show user-friendly error screens instead of red error screens
- Recovery strategies - Automatically retry, reset, or use custom recovery logic
- Error reporting - Plug in Sentry, Crashlytics, or any custom reporter
- Nested boundaries - Isolate errors to specific parts of your widget tree
- Dev mode - Show detailed error info in development, clean UI in production
- Testing utilities - Helpers for testing error boundary behavior
Installation
dependencies:
error_boundary: ^0.1.0
Quick Start
Basic Usage
Wrap any widget that might throw errors:
ErrorBoundary(
child: MyWidget(),
)
Custom Fallback UI
ErrorBoundary(
fallback: (error, retry) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text('Error: ${error.message}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: retry,
child: const Text('Retry'),
),
],
),
),
child: MyWidget(),
)
Extension Syntax
For cleaner code, use the extension method:
MyWidget().withErrorBoundary(
fallback: (error, retry) => Text('Something went wrong'),
)
Recovery Strategies
No Recovery (Default)
Shows fallback until manual retry:
ErrorBoundary(
recovery: const RecoveryStrategy.none(),
child: MyWidget(),
)
Auto-Retry
Automatically retry with exponential backoff:
ErrorBoundary(
recovery: const RecoveryStrategy.retry(
maxAttempts: 3,
delay: Duration(seconds: 1),
backoff: true, // Exponential backoff
),
child: MyWidget(),
)
Reset
Completely rebuild the child widget tree:
ErrorBoundary(
recovery: const RecoveryStrategy.reset(),
child: MyWidget(),
)
Custom Recovery
Implement your own recovery logic:
ErrorBoundary(
recovery: RecoveryStrategy.custom(
onRecover: () async {
await clearCache();
return true; // Return true to retry
},
),
child: MyWidget(),
)
Error Reporting
Console Reporter (Built-in)
Logs errors to the console with detailed information:
ErrorBoundary(
reporters: [ConsoleReporter()],
child: MyWidget(),
)
Sentry Reporter (Built-in)
Send errors to Sentry for monitoring:
// 1. Add sentry_flutter to pubspec.yaml
// dependencies:
// sentry_flutter: ^7.0.0
// 2. Initialize Sentry in main.dart
await SentryFlutter.init(
(options) => options.dsn = 'YOUR_DSN',
appRunner: () => runApp(MyApp()),
);
// 3. Use the reporter
ErrorBoundary(
reporters: [
SentryReporter(
captureException: Sentry.captureException,
configureScope: Sentry.configureScope,
),
],
child: MyWidget(),
)
With filtering:
SentryReporter(
captureException: Sentry.captureException,
configureScope: Sentry.configureScope,
beforeSend: (info) {
// Skip non-critical errors
if (info.severity == ErrorSeverity.low) return null;
return info;
},
environment: 'production',
release: '1.0.0',
)
Firebase Crashlytics Reporter (Built-in)
Send errors to Firebase Crashlytics:
// 1. Add firebase_crashlytics to pubspec.yaml
// dependencies:
// firebase_crashlytics: ^3.0.0
// 2. Initialize Firebase in main.dart
await Firebase.initializeApp();
// 3. Use the reporter
final crashlytics = FirebaseCrashlytics.instance;
ErrorBoundary(
reporters: [
FirebaseCrashlyticsReporter(
recordError: crashlytics.recordError,
setCustomKey: crashlytics.setCustomKey,
setUserIdentifier: crashlytics.setUserIdentifier,
log: crashlytics.log,
),
],
child: MyWidget(),
)
Custom Reporter
Implement ErrorReporter for your own error tracking service:
class MyAnalyticsReporter implements ErrorReporter {
@override
Future<void> report(ErrorInfo info) async {
await myAnalytics.trackError(
error: info.error.toString(),
stackTrace: info.stackTrace.toString(),
severity: info.severity.name,
);
}
@override
void setUserIdentifier(String? userId) {
myAnalytics.setUserId(userId);
}
@override
void setCustomKey(String key, dynamic value) {
myAnalytics.setProperty(key, value);
}
}
Multiple Reporters
Use multiple reporters simultaneously:
ErrorBoundary(
reporters: [
ConsoleReporter(), // Dev logging
SentryReporter(...), // Error monitoring
FirebaseCrashlyticsReporter(...), // Crash reporting
],
child: MyWidget(),
)
Or use CompositeReporter:
final reporter = CompositeReporter([
ConsoleReporter(),
SentryReporter(...),
]);
ErrorBoundary(
reporters: [reporter],
child: MyWidget(),
)
Nested Boundaries
Error boundaries can be nested to isolate errors:
ErrorBoundary(
onError: (e, s) => print('Outer caught: $e'),
child: Column(
children: [
SafeWidget(),
ErrorBoundary(
child: DangerousWidget(), // Errors here won't affect SafeWidget
),
],
),
)
Error Bubbling
Use shouldRethrow to bubble errors up to parent boundaries:
ErrorBoundary(
shouldRethrow: (error) => error is CriticalError,
child: MyWidget(),
)
Async Error Catching
By default, errors in Futures and Streams within the boundary are caught:
ErrorBoundary(
catchAsync: true, // Default
child: MyAsyncWidget(),
)
Disable if you handle async errors differently:
ErrorBoundary(
catchAsync: false,
child: MyWidget(),
)
Testing
The package includes testing utilities:
import 'package:error_boundary/error_boundary.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('shows fallback on error', (tester) async {
final tracker = ErrorTracker();
await tester.pumpWidget(
MaterialApp(
home: ErrorBoundary(
onError: tracker.onError,
child: ThrowingWidget(error: Exception('Test')),
),
),
);
await tester.pumpAndSettle();
expect(tracker.hasErrors, isTrue);
expect(tracker.lastError?.message, contains('Test'));
expect(find.text('Something went wrong'), findsOneWidget);
});
}
Test Helpers
ErrorTracker- Captures errors for assertionsThrowingWidget- Widget that throws during buildAsyncThrowingWidget- Widget that throws async errors
API Reference
ErrorBoundary
| Property | Type | Default | Description |
|---|---|---|---|
child |
Widget |
required | Widget to wrap |
fallback |
FallbackBuilder? |
null | Custom fallback UI builder |
onError |
ErrorCallback? |
null | Called when error occurs |
reporters |
List<ErrorReporter> |
[] |
Error reporters |
recovery |
RecoveryStrategy |
none() |
Recovery strategy |
shouldRethrow |
ShouldRethrow? |
null | Whether to bubble errors |
catchAsync |
bool |
true |
Catch async errors |
devMode |
bool? |
kDebugMode |
Show detailed errors |
ErrorInfo
| Property | Type | Description |
|---|---|---|
error |
Object |
The error object |
stackTrace |
StackTrace |
Stack trace |
severity |
ErrorSeverity |
low, medium, high, critical |
type |
ErrorType |
build, runtime, async, etc. |
source |
String? |
Error source identifier |
timestamp |
DateTime |
When error occurred |
context |
Map<String, dynamic> |
Additional context |
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
License
BSD 3-Clause License - see LICENSE for details.
Libraries
- error_boundary
- A robust error boundary widget for Flutter.