Global Error Handling

Medium PriorityAsked in ~55% of mid-level interviews

3 min read

Architecture

Source of errorHook
Framework / widget build / layout / paintFlutterError.onError
Uncaught async errors in Dart (Future, Stream not handled)PlatformDispatcher.instance.onError
Replace the default red error widget UIErrorWidget.builder
Errors in a runZonedGuarded zonerunZonedGuarded(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 levelCatch withThen do
Network failures, validation, parse errorstry/catch at call siteShow friendly UI, surface state
Unexpected exceptions in build/layoutFlutterError.onErrorLog + degraded UI
Forgot-to-await Futures, stream errorsPlatformDispatcher.onErrorLog; user usually sees nothing
Production red-screenErrorWidget.builderGeneric "something went wrong" UI
Native iOS/Android crashNative 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

  1. What's the difference between FlutterError.onError and PlatformDispatcher.onError? FlutterError.onError catches synchronous errors inside Flutter's framework callbacks (build, layout, paint, gesture). PlatformDispatcher.onError catches uncaught async errors at the Dart isolate level — Futures and Streams whose errors weren't handled. You want both wired up.

  2. How do you handle errors in async code that you await? Standard try/catch. The point of the global hooks is to catch what you forgot to handle. Production-quality code wraps every async boundary in a try/catch that maps to a typed error and reports if it's unexpected.

  3. What is runZonedGuarded and 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.onError covers most use cases more cleanly. Still useful for isolating a sub-process (e.g., a plugin's callbacks) into its own zone.

  4. 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 redact callback. 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.