Global Error Handling
3 min read
Architecture
| Source of error | Hook |
|---|---|
| Framework / widget build / layout / paint | FlutterError.onError |
| Uncaught async errors in Dart (Future, Stream not handled) | PlatformDispatcher.instance.onError |
| Replace the default red error widget UI | ErrorWidget.builder |
Errors in a runZonedGuarded zone | runZonedGuarded(body, onError) (older pattern; PlatformDispatcher.onError covers most cases since Flutter 3.3) |
| Native crashes (iOS/Android) | Native crash reporting SDK (Crashlytics native integration) |
Wire all three up at app startup so nothing falls through.
Code in action — minimum production setup
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await CrashReporting.init();
// 1. Framework errors
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.presentError(details); // still log to console
CrashReporting.recordFlutterError(details); // report
};
// 2. Uncaught async errors
PlatformDispatcher.instance.onError = (error, stack) {
CrashReporting.recordError(error, stack);
return true; // signal handled, don't crash
};
// 3. Replace the red error widget in release builds
ErrorWidget.builder = (FlutterErrorDetails details) {
if (kReleaseMode) {
return const Material(child: Center(child: Text('Something went wrong')));
}
return ErrorWidget(details.exception); // default in debug
};
runApp(const MyApp());
}
// Optional: per-subtree boundary (for risky features)
class ErrorBoundary extends StatefulWidget {
const ErrorBoundary({super.key, required this.child, required this.onError});
final Widget child;
final Widget Function(Object error, StackTrace? stack) onError;
@override
State<ErrorBoundary> createState() => _ErrorBoundaryState();
}
class _ErrorBoundaryState extends State<ErrorBoundary> {
Object? _err; StackTrace? _stack;
@override
void initState() {
super.initState();
final prev = FlutterError.onError;
FlutterError.onError = (details) {
prev?.call(details); // chain to global handler
setState(() { _err = details.exception; _stack = details.stack; });
};
}
@override
Widget build(BuildContext context) =>
_err != null ? widget.onError(_err!, _stack) : widget.child;
}
Where to handle what
| Error level | Catch with | Then do |
|---|---|---|
| Network failures, validation, parse errors | try/catch at call site | Show friendly UI, surface state |
| Unexpected exceptions in build/layout | FlutterError.onError | Log + degraded UI |
| Forgot-to-await Futures, stream errors | PlatformDispatcher.onError | Log; user usually sees nothing |
| Production red-screen | ErrorWidget.builder | Generic "something went wrong" UI |
| Native iOS/Android crash | Native SDK (Crashlytics) | Report; restart |
Common mistakes to avoid
// ❌ Catching errors and silently swallowing them
try { await api.call(); } catch (_) {} // no log, no UI — invisible bug
// ✅ Log to crash reporting + show user feedback
// ❌ Letting the red ErrorWidget ship to production
// ✅ ErrorWidget.builder + a friendly fallback
// ❌ Only catching framework errors, ignoring async
// FlutterError.onError doesn't cover uncaught Future errors
// ✅ Also wire PlatformDispatcher.instance.onError
// ❌ Doing heavy work in the error handler
FlutterError.onError = (d) async { await uploadLog(d); }; // blocks frames
// ✅ Schedule reporting on a microtask / fire-and-forget
// ❌ Reusing one global state for "error boundary" across the app
// ✅ Scope ErrorBoundaries around risky features (a chart, a payment flow, a video player)
// ❌ Not testing error paths
// Force errors in dev (`throw Exception('test')`) to verify the funnel works end-to-end
Interview follow-ups
-
What's the difference between
FlutterError.onErrorandPlatformDispatcher.onError?FlutterError.onErrorcatches synchronous errors inside Flutter's framework callbacks (build, layout, paint, gesture).PlatformDispatcher.onErrorcatches uncaught async errors at the Dart isolate level — Futures and Streams whose errors weren't handled. You want both wired up. -
How do you handle errors in async code that you
await? Standardtry/catch. The point of the global hooks is to catch what you forgot to handle. Production-quality code wraps every async boundary in atry/catchthat maps to a typed error and reports if it's unexpected. -
What is
runZonedGuardedand is it still needed? It's a wrapper that catches errors in everything running inside its zone (top-level async errors, isolate errors). Since Flutter 3.3,PlatformDispatcher.onErrorcovers most use cases more cleanly. Still useful for isolating a sub-process (e.g., a plugin's callbacks) into its own zone. -
How do you report errors with PII safety? Strip user data before sending to crash reporting: avoid logging request bodies, email addresses, tokens. Most SDKs let you set a
redactcallback. For Flutter errors, the stack trace is fine; for custom logs, treat them like PII data — opt-in fields only.
How helpful was this content?
Please sign in to rate this article.