How to catch async exception in one place (like main) and show it in AlertDialog?

864

By default, if there is an uncaught exception in a Flutter application, it is passed to FlutterError.onError. This can be overridden with a void Function(FlutterErrorDetails) to provide custom error handling behaviour:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  FlutterError.onError = (details) {
    print(details.exception);  // the uncaught exception
    print(details.stack)  // the stack trace at the time
  }
  runApp(MyApp());
}

If you want to show a dialog in this code, you will need access to a BuildContext (or some equivalent mechanism to hook into the element tree).

The standard way of doing this is with a GlobalKey. Here, for convenience (because you want to show a dialog) you can use a GlobalKey<NavigatorState>:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  final navigator = GlobalKey<NavigatorState>();
  FlutterError.onError = (details) {
    navigator.currentState!.push(MaterialPageRoute(builder: (context) {
      // standard build method, return your dialog widget
      return SimpleDialog(children: [Text(details.exception.toString())]);
    }));
  }
  runApp(MyApp());
}

Note that if you need a BuildContext inside your onError callback, you can also use navigator.currentContext!.

You then need to pass your GlobalKey<NavigatorState> to MaterialApp (or Navigator if you create it manually):

@override
Widget build(BuildContext context) {
  return MaterialApp(
    navigatorKey: navigatorKey,  // pass in your navigator key
    // other fields
  );
}
Share:
864
cheiser
Author by

cheiser

Updated on December 30, 2022

Comments

  • cheiser
    cheiser over 1 year

    Trouble

    I build Flutter app + Dart. Now i am trying to catch all future exceptions in ONE place (class) AND showAlertDialog.

    Flutter Docs proposes 3 solutions to catch async errors:

    1. runZonedGuarded
    2. ... async{ await future() }catch(e){ ... }
    3. Future.onError

    But no one can achieve all of the goals (in its purest form):

    First: can't run in widget's build (need to return Widget, but returns Widget?.

    Second: works in build, but don't catch async errors, which were throwed by unawaited futures, and is"dirty" (forces to use WidgetBinding.instance.addPostFrameCallback. I can ensure awaiting futures (which adds to the hassle), but I can't check does ensures it third-part libraries. Thus, it is bad case.

    Third: is similar to second. And looks monstrous.

    My (bearable) solution

    I get first solution and added some details. So,

    I created ZonedCatcher, which shows AlertDialog with exception or accumulates exceptions if it doesn't know where to show AlertDialog (BuildContext has not been provided). AlertDialog requires MaterialLocalizations, so BuildContext is taken from MaterialApp's child MaterialChild.

    void main() {
      ZonedCatcher().runZonedApp();
    }
    
    ...
    
    class ZonedCatcher {
      BuildContext? _materialContext;
      set materialContext(BuildContext context) {
        _materialContext = context;
        if (_exceptionsStack.isNotEmpty) _showStacked();
      }
    
      final List<Object> _exceptionsStack = [];
    
      void runZonedApp() {
        runZonedGuarded<void>(
          () => runApp(
            Application(
              MaterialChild(this),
            ),
          ),
          _onError,
        );
      }
    
      void _onError(Object exception, _) {
        if (_materialContext == null) {
          _exceptionsStack.add(exception);
        } else {
          _showException(exception);
        }
      }
    
      void _showException(Object exception) {
        print(exception);
        showDialog(
          context: _materialContext!,
          builder: (newContext) => ExceptionAlertDialog(newContext),
        );
      }
    
      void _showStacked() {
        for (var exception in _exceptionsStack) {
          _showException(exception);
        }
      }
    }
    
    
    ...
    
    class MaterialChild extends StatelessWidget {
      MaterialChild(this.zonedCatcher);
    
      final ZonedCatcher zonedCatcher;
    
      @override
      Widget build(BuildContext context) {
        zonedCatcher.materialContext = context; //!
        ...
      }
    }
    
    

    flaws

    1. At this moment I don't know how organize app with several pages. materialContext can be taken only from MaterialApp childs, but pages are set already at the MaterialApp widget. Maybe, I will inject ZonedCatcher in all pages and building pages will re-set materialContext. But I probably will face with GlobalKey's problems, like reseting materialContext by some pages at the same time on gestures.
    2. It is not common pattern, I have to thoroughly document this moment and this solution makes project harder to understand by others programmists.
    3. This solution is not foreseen by Flutter creators and it can break on new packages with breaking-changes.

    Any ideas?

    • CrimsonFoot
      CrimsonFoot almost 3 years
      I don't know why you try to simplify catching exceptions too much but I think if you use GetX package, you can show dialog without context. I don't think this is a smooth solution but it seems to work
  • LearnFlutter
    LearnFlutter about 2 years
    Im having a similiar issue here stackoverflow.com/questions/71110229/…