Flutter: remove the gray screen of death in production

Flutter: remove the gray screen of death in production

Introduction – why the grey screen is a problem

When the build, layout or paint of a widget fail, Flutter calls ErrorWidget.builder.
In debug, the framework shows a red rectangle detailing the stack trace; in release, it renders a neutral grey background to avoid exposing sensitive information to the public (Handling errors in Flutter).
Unsightly and textless, this screen does not reassure the user!

Understanding the Flutter error handling chain

This section clarifies who catches the error, when, and how to customize the response.

1. FlutterError.onError : the first link

  • Role : global handler invoked for any error detected on the Flutter stack.
  • Default : calls FlutterError.presentError, which simply prints the log.
  • Customization : replace it with your function; you can still call presentError to keep the console trace (onError property - FlutterError class - foundation library - Dart API).
  • Scope : synchronous framework errors, including those triggered during setState, build, layout or paint.

🧠 Good to know – Because it runs before painting, onError lets you route the exception to Crashlytics while leaving rendering to ErrorWidget.builder.

2. ErrorWidget.builder : the screen that replaces the faulty widget

  • When is it called ? When an exception occurs during the build phase.
  • Default behavior :
    • Debug/Profile : red rectangle + message.
    • Release : plain grey background (aka grey screen of death) (Handling errors in Flutter).
  • Customization : define your own builder to return a UX-friendly component — clear text, sufficient contrast, back button.

3. Dart Zones and runZonedGuarded : catching asynchronous errors

4. PlatformDispatcher.onError : last native safeguard

  • Context : an error can originate from a plugin or a method channel executed on the engine side (C++/JNI).
  • Callback : PlatformDispatcher.instance.onError receives the exception and the native stack.
  • Convention : return true if you have handled the problem; otherwise Flutter will terminate the process (Handling errors in Flutter).

➡️ Temporal sequence

StepWho captures?Example errorCustomization possibilities
1FlutterError.onErrorsetState on a disposed widgetLog + trigger Crashlytics
2ErrorWidget.builderDivision by zero in build()Render a fallback screen
3runZonedGuardedTimeout in a FutureAutomatic retry of the zone
4PlatformDispatcher.onErrorAndroid NullPointerExceptionDisplay of a native fallback screen

Thus, no exception should reach the user without passing through at least one of these hooks.

Step-by-step tutorial

1. Initialize a global Error Boundary (main.dart)

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 1. Capture Flutter synchrones
  FlutterError.onError = (details) {
    FlutterError.presentError(details);          // Log locale
    _report(details.exception, details.stack);   // Crashlytics / Sentry
  };

  // 2. Capture erreurs natives (plugins, channels)
  PlatformDispatcher.instance.onError = (error, stack) {
    _report(error, stack);
    return true;                                 // Erreur considérée gérée
  };

  // 3. Capture erreurs asynchrones hors pile Flutter
  runZonedGuarded(
    () => runApp(const MyApp()),
    (error, stack) => _report(error, stack),
  );
}

void _report(Object error, StackTrace? stack) {
  // TODO : intégrer FirebaseCrashlytics.instance.recordError(...)
}

This triple interception covers the cases documented by the Flutter team (Common Flutter errors, Handling errors in Flutter).

2. Create an accessible CustomErrorScreen (WCAG 2.2)

class CustomErrorScreen extends StatelessWidget {
  final Object error;
  const CustomErrorScreen(this.error, {super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Center(
        child: Semantics(
          label: 'Erreur critique',
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 320),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Icon(Icons.error_outline, size: 96),
                const SizedBox(height: 24),
                Text(
                  'Oups ! Un incident est survenu.',
                  style: Theme.of(context).textTheme.headlineSmall,
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 12),
                Text(
                  'Notre équipe a été notifiée. Vous pouvez relancer l'application.',
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 32),
                ElevatedButton(
                  onPressed: () => runApp(const MyApp()),
                  child: const Text('Redémarrer'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
ErrorWidget.builder = (details) => CustomErrorScreen(details.exception);

3. Log to Crashlytics or Sentry

import 'package:firebase_crashlytics/firebase_crashlytics.dart';

void _report(Object error, StackTrace? stack) {
  FirebaseCrashlytics.instance.recordError(error, stack);
  // Ou: Sentry.captureException(error, stackTrace: stack);
}

These platforms aggregate the error, the stack, the device context and trigger real-time alerts.

4. Unit & widget tests

testWidgets('CustomErrorScreen remplace le widget fautif', (tester) async {
  ErrorWidget.builder = (d) => CustomErrorScreen(d.exception);
  await tester.pumpWidget(const BrokenWidget());   // Simule un widget erroné
  expect(find.byType(CustomErrorScreen), findsOneWidget);
});

The Handling errors in Flutter guide details injecting a MaterialApp.builder for more complex tests (Handling errors in Flutter).

Conclusion

Key points

  • The grey screen hides an unhandled exception; it harms trust and retention.
  • Combine FlutterError.onErrorErrorWidget.builderrunZonedGuardedPlatformDispatcher.onError to capture all scenarios.
  • Display a CustomErrorScreen compliant with WCAG, then report the incident to Crashlytics or Sentry.

Tags

  • flutter

  • iOS

  • android

  • dart

  • firebase

  • crashlytics

  • tests

This article was posted on

Comments

Loading...

Flutter: remove the gray screen of death in production | DEMILY Clément