Hello, friends. Just now the project manager asked me what happened to the wandering teacher. I said what happened and sent me some screenshots. I have a look at! Ouch! Yuase Sakata, I have two requests coming online. One demand PRD more than 90 kg, one demand PRD more than 80 kg. Tamen says, ah… I got over 30 abnormal alerts online, so LET me run it down, and I said yes. Then I a checked exception log, ah, soon found a null pointer exception, an abnormal type conversion, there are also several official issue, I have lived all catch, catch live, nature is the exception handling in accordance with the traditional way, to print the exception information, I smiled to close the computer, because the traditional method of exception handling, I have dealt with it very reasonably. I caught it and did not report to the server. This is my last tenderness towards the abnormality. When I closed the computer and prepared to go off work, suddenly another abnormal alarm came, I was careless, this abnormal did not catch, and our back-end service crashed, but it doesn’t matter…

Joking aside, aren’t a lot of the exception handling details a little too realistic? We should always be in awe of exception handling.

preface

The Flutter Dart exception is very different from the traditional native platform exception, where tasks are scheduled using multiple threads. When an uncaught exception occurs in one thread, the entire process exits. Dart is single-threaded, tasks are scheduled in an event loop, and Dart exceptions do not crash the application. Instead, code following the current event is not executed.

The advantage of this is that some trivial exceptions do not flash out and the user can continue to use the core functions.

The disadvantage is that these exceptions may have no obvious hint or abnormal performance, which makes the problem easy to be hidden. If the exception happens to be on the core process and the link is long, it may be very difficult to rectify the problem.

This paper will systematically introduce the correct methods of exception handling from the perspectives of exception capturing, processing, prompting, reporting and stability indicators, so as to help us improve the stability of APP. I hope it will be helpful to you.

Local exception capture

Synchronous abnormal

For synchronous exceptions, we only need a try-catch layer on the block where the exception may occur.

try {
  String abc;
  print("abc's length ${abc.length}");
} catch (error, stacktrace) {
  //todo catch all error
}
Copy the code

Catch provides a maximum of two optional parameters:

  • The first argument, error, is of type Object, meaning that an exception can throw any Object.
  • The second parameter, StackTrace, represents the exception stack.

If you want to catch specific types of exceptions, use the on keyword.

try {
  String abc;
  print("abc's length ${abc.length}");
}  on NoSuchMethodError catch (error, stacktrace) {
  //todo catch NoSuchMethodError
}
Copy the code

In handling exceptions, new exceptions can be thrown, using the throw keyword.

try {
  String abc;
  print("abc's length ${abc.length}");
}  on NoSuchMethodError catch (error, stacktrace) {
  //exception handler
  ...
  throw 'abc';
}
Copy the code

Common synchronization exceptions include NoSuchMethodError, type XXX is not a subtype of Type XXX, and FormatException.

Asynchronous exception

CatchError: catchError: Function error; stackstace: error;

Future.delayed(Duration(seconds: 1), () {
  throw '123';
}).then((value) {
  print('value $value');
  return value;
}).catchError((error, stack) {
  print('error $error stack $stack');
});
Copy the code

The second parameter is {bool test(Object error)}, which is a judgment expression. When this expression returns true, it means that the catch logic needs to be executed. If false is returned, the catch logic is not executed, that is, it becomes an uncaught exception. The default value is true.

The function here is to handle exceptions in fine detail, which can be understood as the enhanced version of on keyword in synchronous exception, for example:

Future.delayed(Duration(seconds: 1), () {
  throw 123;
}).then((value) {
  print('value $value');
  return value;
}).catchError(() {
  //todo exception handler
}, test: (error) => error is int);
Copy the code

Note that the try-catch block cannot catch asynchronous exceptions. Synchronous calls declared with the await keyword fall within the scope of synchronous exceptions and can be caught with a try-catch.

Since the asynchronous task is in a different task queue by default, this part of the exception does not affect the UI rendering process, there is no red screen displayed on the page, only output in the console.

Global exception catching

Global exceptions can be classified into global synchronous exceptions and global asynchronous exceptions.

For synchronous exceptions, most of which occur in the UI rendering process, we call them global UI exceptions.

Note that when I say global UI exceptions, I do not refer specifically to UI layout or drawing exceptions, but to exceptions that occur throughout the process. For example, an exception in initState.

Catch exceptions in a global catchError: catch exceptions in a global catchError And can we catch synchronization exceptions in the non-UI drawing process mentioned above?

The answer is yes, let’s take a look at it in turn:

Global UI exception

In the world of Flutter, everything is Widget. Views and business logic are driven by UI rendering. The red screen you see every now and then when you develop Flutter pages is actually the prompt page displayed by the Flutter Framework after catching exceptions generated during the drawing of the UI layout.

# framework.dart / ComponentElement void performRebuild() { ... Widget built; try { built = build(); . } catch (e, stack) { built = ErrorWidget.builder( _debugReportException( ErrorDescription('building $this'), e, stack, ...). ,); } finally { ... } try { _child = updateChild(_child, built, slot); } catch (e, stack) { built = ErrorWidget.builder( _debugReportException( ErrorDescription('building $this'), e, stack, . },),); _child = updateChild(null, built, slot); }}Copy the code

PerformRebuild is called during widget redraw and is a part of the UI rendering process, including the State lifecycle. You can see that the build method and the updateChild update method are try-catted internally.

The widget returned by errorWidget. builder is displayed in the foreground as an error message, which by default is the error page we see with yellow letters on a red background.

The _debugReportException method internally wraps the error into an _debugReportException object and calls the static function onError of the FlutterError object. By default, error information, stack information and other information are printed on the console.

# framework.dart FlutterErrorDetails _debugReportException( DiagnosticsNode context, dynamic exception, StackTrace stack, { InformationCollector informationCollector, }) { final FlutterErrorDetails details = FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets library', context: context, informationCollector: informationCollector, ); FlutterError.reportError(details); return details; } # FlutterError static FlutterExceptionHandler onError = dumpErrorToConsole; static void reportError(FlutterErrorDetails details) { ... if (onError ! = null) onError(details); }Copy the code

Therefore, we can customize the onError reassignment of FlutterError in the main entry.

Void main() {fluttererror.onerror = (FlutterErrorDetails details) {// print('catch an exception'); FlutterError.dumpErrorToConsole(details); }; runApp(MyApp()); }Copy the code

If you actually test this, you will find that this does not catch exceptions. This is because the official issue#47447 of the Flutter works only in release mode, or with the ship’s new version of the Flutter.

Global exception handling is not caught

Flutter provides us with the concept of a Zone, a kind of sandbox that wraps our runApp calls around a runZoned. It can capture all uncaught exceptions globally, including asynchronous exceptions. A Thread of Android. UncaughtExceptionHandler.

# main.dart
runZoned(() {
    runApp(MyApp());
}, onError: _handleError);

void _handleError(Object obj, StackTrace stack) {
  // todo global uncaught exception handle
  print('zone _handleError $obj stack $stack');
}
Copy the code

At this point, we are done catching all Dart exceptions in the APP.

About the Zone

The Zone in Dart indicates the environment in which the code is executed. The main thread also starts a new sandbox environment using the _runMainZoned method. The runZoned function forks a new sandbox environment based on the current Zone. In this sandbox environment, you can do centralized exception handling or change some of the default system behavior.

# Zone.dart
R runZoned<R>(R body(),
    {Map zoneValues,
    ZoneSpecification zoneSpecification, 
    Function onError})
Copy the code
  • ZoneValues: private data of a Zone. By default, the zoneValues specified by the parent Zone node can be accessed by the child Zone. We can get a reference to the parent Zone using the.parent methodZone.current.parent. In addition, the zoneValues can pass through any location in the current zoneZone.current[#key]Access the value.
  • ZoneSpecification: You can set the interception of some system behaviors, such as global event callback, global exception callback, Timer creation interception, print interception (log collection or beautification printing), etc. For details, see zoneSpecification. Class
  • OnError: The global uncaught exception mentioned above will receive a callback here. For caught exceptions, we can also passZone.current.handleUncaughtError(exception, stack)Methods are sent to this onError for centralized processing.

For example, the global UI exception mentioned above is caught by the Flutter Framework. So we can collect this part of the exception through the following code.

FlutterError.onError = (FlutterErrorDetails details) async {
    Zone.current.handleUncaughtError(details.exception, details.stack);
};
Copy the code

Note that the Future in Flutter encapsulates the Zone.

Exception information and report collection

Exception capture, only completed the first step of the long march, you can see UI rendering exceptions will have obvious view prompt, but for asynchronous exceptions are not aware of the development and testing, unlike native development, abnormal APP will directly flash back.

This makes it easy to ignore exceptions. In order to expose problems as early as possible, we must give strong hints to test and development when we catch exceptions.

Custom exception prompt view

The default behavior of Flutter is to display a red screen with yellow words when an exception occurs in the UI rendering. We can customize an error alert page in the main function of Flutter. Here is an example:

# main.dart
ErrorWidget.builder = (FlutterErrorDetails detail) {
return Container(
    color: Colors.white,
    child: Column(children: <Widget>[
      Text(
        'error\n--------\n ${detail.exception}',
        style: TextStyle(fontSize: 12, color: Colors.green),
      ),
      SizedBox(height: 12),
      Text(
        'stacktrace\n--------\n ${detail.stack}',
        style: TextStyle(fontSize: 12, color:Colors.green),
      ),
    ]));
};
Copy the code

Does it look much calmer than a red screen?

Global prompt pop-up window

For other exceptions, they are not displayed on the page by default, and we can alert developers and testers to these exceptions in the form of pop-ups.

It can be done as follows:

runZoned(() { runApp(MyApp()); }, onError: (exception, stackTrace) { _handleError(exception, stackTrace); }); Void _handleError(Object error, StackTrace stack, Map zoneValues) {debugPrint(error); _showExceptionAlertDialog(error, stack); _uploadException(error, stack); // Reporting an exception}Copy the code

The popover code is as follows:

Void showExceptionAlertDialog(Object Error, StackTrace stack) {// assert(() {GlobalKey key = GlobalKey; if (key == null || key.currentContext == null) { return true; } / / (2) SchedulerBinding. Instance. AddPostFrameCallback (_) {showDialog (context: key currentContext, / / (3) the builder: (BuildContext context) { return AlertDialog( title: Text('$error'), content: SingleChildScrollView( child: Text("$stack"), ), actions: ... ,); }); }); return true; } ()); }Copy the code

The end result looks like this:

Here are three things to note:

  1. With assert assertions, only execute in debug mode, so the Release version does not pop up an exception dialog.
  2. Since an exception can occur at any time, and the view may be rendering, you need to register an end-of-render callback to execute after this frame is rendered. Exceptions in the render phase will pop up immediately after the end of this frame, while asynchronous exceptions will pop up after the end of the next frame if they appear after the end of the current frame.
  3. Pop-ups need to specify a context that determines which navigation stack they belong to, in this case using the global GlobalKey, which can normally be declared using the GlobalKey of the Widget pointed to by home in the MaterialApp. And the whole point of doing that is to make sure that the home never exits, that the context exists.

To improve portability, I extracted the exception callback and provided some switches for the business layer to use flexibly. For details, refer to Github Flutter_Exception_handler.

For exception reporting, if you do not have your own exception collection platform, you can use Bugly or the open source Sentry service.

Now that we have captured and reported exceptions, how do we evaluate the stability of the project?

Stability index

For native development, stability can be measured by the crash rate, and a crash rate of 1 in 10,000 is considered good.

For Flutter, however, crashes only occur in the Flutter engine or in some channels. Dart side exceptions do not cause application crashes. Most business exceptions are on Dart side. Therefore, the traditional Crash rate must be significantly reduced (there may be some strange Flutter Engine crashes, so it is recommended to upgrade to the latest Flutter version).

In contrast, the Dart exception rate can be ridiculously high because a single start can result in multiple exceptions, and even higher if an exception occurs during a timed task. Therefore, in order to more reasonably measure the stability of Flutter side, we can count the page anomaly rate.

Page exception rate = Number of exceptions/Number of opened pages

For the number of open pages, we can register navigatorObservers with The MaterialApp to listen for global page routing data. When an exception is reported, the current route information is also included, so that the page exception rate can be counted.

We can calculate the anomaly rate of a page in a more refined way. Theoretically, this value may also exceed 100%, but compared with the statistical method of crash rate, it has been substantially more reasonable.

For business scenarios with heavy Flutter, the page exception rate should also be 1 in 10,000 as a good standard.

Common exception handling Tips

Here are a few common exceptions to work with for your reference and attention in real development.

  1. How to use?And operators??To perform an air check operation.
  2. In Dart, everything is an object. Common ints and bool are objects, and the default value is null if you’re not careful to writeif(isComplete)bool isExceed = count < 10Code like this can go wrong, and the right thing to do is to say,if(isComplete ?? false)bool isExceed = (count ?? 0) < 10.
  3. SetState () ensures that the value is mounted.
  4. Dispose is called after the ScrollController is used up. Do not use the ScrollController again after it has been destroyed.
  5. For digital conversion scenarios, instead of int.parse directly, you can use int.tryparse, which internally handles exceptions.
  6. Text The specified Text cannot be null. Null verification is required.
  7. PlatformChannel should register in advance and implement method.
  8. For example, the home page requests three network interfaces at the same time, but the core interface is the first one, so the first and the last two should be handled separately instead of catching the whole process together.
  9. Upgrade to the latest version of Flutter as much as possible. When you actually get the exception data online, you will find that many of the exceptions are official Flutter issues.
  10. For scenes that rely heavily on Webview, please upgrade to version 1.20 or later as soon as possible, and use HybridComposition mode to render Webview to avoid weird problems such as keyboard, Resize and multi-finger operation.
  11. Exceptions that have been caught manually in the business code should be actively reported to a separate exception type to distinguish them from uncaught exceptions.

Related articles

  • Report errors to a service
  • Dart Zones

Please check out wanderingTech for more in-depth articles