avatarGreg Perry

Summary

The provided content introduces an Error Handler class for Flutter applications, which allows developers to implement custom error handling and widgets to manage errors more effectively within their apps.

Abstract

The article discusses an Error Handler class for Flutter, which is designed to give developers the ability to define custom error handling routines and error widgets for their applications. It emphasizes the importance of error handling in Flutter apps, especially for larger projects involving multiple developers. The class, available on GitHub, includes methods to instantiate, set, and dispose of error handlers, allowing for modular error management. The article also covers how to use the ErrorHandler class within State objects, providing an example of how to instantiate and dispose of an error handler in a State object's lifecycle. Additionally, it explains the use of a static runApp function to define error handling for the entire application and discusses the implementation of error callbacks using Flutter's onError routine, Isolates, and Zones to catch a wide range of errors. The article encourages developers to take control of error handling in their apps and provides a default error widget as a starting point for customization.

Opinions

  • The author suggests that while the Error Handler class may be most useful in larger apps with multiple developers, it can still provide valuable ideas on error handling to any Flutter developer.
  • The author expresses a preference for having options, particularly when it comes to error handling, and emphasizes the modularity and flexibility provided by the Error Handler class.
  • The author encourages developers to actively handle errors in their apps, considering them an inevitability rather than an exception.
  • The article implies that the provided Error Handler class is a starting point and encourages developers to modify and improve the default error widget to suit their needs.
  • The author acknowledges the complexity of error handling, especially when dealing with Isolates and Zones, but also emphasizes the importance of understanding and implementing comprehensive error handling strategies.

An Error Handler for Flutter

A way to implement error handling in your Flutter app.

Let’s take a closer look at the ‘Error Handler’ first introduced at the end of a previous article, Error Handling in Flutter. Note, it’s suggested you read this previous article first to get up to speed. This class allows you to introduce your own error-handler to either your app as a whole or to only segments of your code. Admittedly, such an ability may only be useful to a larger app involving multiple developers, but it’s hoped the class will at least give you some ideas on the subject of Error Handling.

In this quick little article, we will walk through parts of the class explaining the role they play in providing error handling. You will be presented with some sample code to demonstrate how the class is implemented. Of course, the class, ErrorHandler, is available to you on Github. Take it and make it your own. Error handling is a necessity in every app. If anything, maybe this article will get you to start thinking about handling errors. Do it! Handle your errors. They will occur!

Let’s Begin.

Other Stories by Greg Perry

At the beginning of the class, ErrorHandler, you can see both the original ‘error handler’ and ‘ErrorWidget’ routines that are currently called when an error occurs are recorded in package-private variables in the class’ constructor. As a reminder, the ‘ErrorWidget’ is that wonderful ‘Red Screen of Doom’ that appears when something bad happens while developing.

class ErrorHandler {
  //
  ErrorHandler({
    FlutterExceptionHandler handler,
    ErrorWidgetBuilder builder,
  }) {
    _oldOnError = FlutterError.onError;
    _oldBuilder = ErrorWidget.builder;

    set(builder: builder, handler: handler);
  }

With the two parameters, handler & builder, passed to the constructor being ‘named parameters’, this means you've got the option to provide your own error handler and ‘error widget’ right when you first instantiate an ErrorHandler object. That’s something of note. Doing so, right away defines the ‘error handler’ and ‘ErrorWidget’ routines to be used at that point.

Those named parameters further imply, of course, you’ve other means to assign these options. In fact, you can see another option right inside the constructor itself. There’s the public function, set(). You could use that function instead — it too has the two named parameters. You could assign either one with that function. You have that option.

Further along in the code, there’s the function, dispose(). As you would suspect, this function restores the original ‘handler’ and ‘builder’ when you’re ‘exiting’ the segment of code using that particular error handler and or error widget.

void dispose() {
  // Restore the error widget routine.
  if (_oldBuilder != null) ErrorWidget.builder = _oldBuilder;
  // Return the original error routine.
  if (_oldOnError != null) FlutterError.onError = _oldOnError;
}

Now, why are you allowed to instantiate more than one error handler? Well, so you have options. If you know me, you know I love options. Each State object in a Flutter app, for example, presents the user with an interface. In many cases, much of the code for that interface and for that particular part of the app will be found in that very State object. Hence, there may be instances where you would like a particular State object to having its own error handler. It could happen?!

At least, with this ‘error handler’ class, you have that option. There’s an example below. You can see an error handler is instantiated in the State object’s initState() function. Note, when the State object terminates, the error handler’s function, dispose(), is called in the State object’s own dispose() function as well — to restore the original error handling. See how that works? Very modular. It’s an option.

class _DetailsScreenState extends State<DetailsScreen> {
  int _selectedViewIndex = 0;

  @override
  void initState(){
    super.initState();
    handler = ErrorHandler.init();
  }
  ErrorHandler handler;

  @override
  void dispose(){
    handler.dispose();
    super.dispose();
  }

You may have noticed in the example above, a named constructor, ErrorHandler.init(), was used instead of the more commonly used generative constructor. It turns out, as you see below, this constructor actually calls the original constructor as well but explicitly supplies an ‘Error Widget’ to the named parameter, builder. It’s a default version for you to use in the form of the package-private function, _defaultErrorWidget(). It simply presents a light-gray screen if there’s a crash and not that red one. That’s all.

ErrorHandler.init() {
    /// Assigns a default ErrorWidget.
    ErrorHandler(
        builder: (FlutterErrorDetails details) => _defaultErrorWidget(details));
  }

You’re free to use the original constructor, of course, and assign your own ‘ErrorWidget.’ Heck! You have a copy of the class itself — change the function, _defaultErrorWidget! This copy is merely for demonstrating what can be done to implement error handling in your app. Make it your own!

Run Your App To Handle Errors

There is one static member in the class, ErrorHandler, that you may find useful. It’s a static function that defines an error handler for the whole application. You will recognize this function’s name. It’s called runApp().

void main() => ErrorHandler.runApp(MyApp());

There’s a lot going on this static function. It ensures that any ‘sort of error’ is caught by the specified error handler. Further, you have the option to pass in an instantiated version of this very class as an object. This is so to assign your own ‘error handling’ routines to the whole app. I’ll show you how that’s done shortly. For now, let’s take a look at the runApp() function.

/// Wrap the Flutter app in the Error Handler.
static void runApp(Widget myApp, [ErrorHandler handler]) {
  // Can't be used properly if being called again.
  if (ranApp) return;

  ranApp = true;
  // Catch any errors in Flutter.
  handler ??= ErrorHandler();

  // Catch any errors in the main() function.
  Isolate.current.addErrorListener(new RawReceivePort((dynamic pair) async {
    var isolateError = pair as List<dynamic>;
    _reportError(
      isolateError.first.toString(),
      isolateError.last.toString(),
    );
  }).sendPort);

  // To catch any 'Dart' errors 'outside' of the Flutter framework.
  runZoned<Future<void>>(() async {
    w.runApp(myApp);
    // Catch any errors in the error handling.
  }, onError: (error, stackTrace) {
    _reportError(error, stackTrace);
  });
}

There are three ‘error handler callbacks’ being introduced to your Flutter app in this static function. The first one defines the exception handler, Flutter.onError, and the ‘error widget’, ErrorWidget.builder. This involves the parameter, handler. If no parameter is passed, the variable, handler, is assigned an instantiated object. Doing so will define a ‘default’ error handler and ‘ErrorWidget’ routine to be used at this point.

// Catch any errors in Flutter.
handler ??= ErrorHandler();

The second callback provides ‘error handling’ for any code that may error within the main() entry function itself. This involves Isolates and Zones and is beyond the scope of this article (beyond me for that matter), but, as far as I understand it at this time, that’s what it does.

Isolate.current.addErrorListener(new RawReceivePort((dynamic pair) async {
    var isolateError = pair as List<dynamic>;
    _reportError(
      isolateError.first.toString(),
      isolateError.last.toString(),
    );
  }).sendPort);

The last callback allows you to catch any errors ‘outside’ the Flutter framework — any Dart errors in the Flutter Exception handler itself for example. It does this by, instead of running the Flutter app in the ‘root zone’ — like the main() entry function, a ‘child’ zone is created to run the Flutter application. Again, as far as I currently understand it, a Zone is a scheduled memory task; a separate process that runs without interruption from start to finish. Regardless, any errors not covered by the ‘Flutter’ exception handler, FlutterError.onError, are caught with the ‘onError’ clause seen below

// To catch any 'Dart' errors 'outside' of the Flutter framework.
  runZoned<Future<void>>(() async {
    w.runApp(myApp);
    // Catch any errors in the error handling.
  }, onError: (error, stackTrace) {
    _reportError(error, stackTrace);
  });

Note, all three callbacks call on the same routine called, _reportError(). This routine takes in two parameters, an ‘error’ object, and a ‘stack trace’ object, and creates a FlutterErrorDetails object. It is that object that is passed to the static function, FlutterError.reportError().

/// Produce the FlutterErrorDetails object and call Error routine.
static FlutterErrorDetails _reportError(
  dynamic exception,
  dynamic stack,
) {
  final FlutterErrorDetails details = FlutterErrorDetails(
    exception: exception,
    stack: stack is String ? StackTrace.fromString(stack) : stack,
    library: 'error_handler.dart',
  );

  // Call the Flutter 'onError' routine.
  FlutterError.reportError(details);
  // Returned to the ErrorWidget object in many cases.
  return details;
}

Deep in the Flutter framework in the file, assertions.dart, you can see what this function does in turn — it calls the FlutterExceptionHandler static function, onError, if any. See that ‘if’ statement? That tells you you could assign null to FlutterError.onError and ignore any Flutter errors. Don’t do that. Of course, a Flutter Exception Handler is assigned back in the ErrorHandler class function, set(). Remember?

/// Calls [onError] with the given details, unless it is null.
static void reportError(FlutterErrorDetails details) {
  assert(details != null);
  assert(details.exception != null);
  if (onError != null)
    onError(details);
}

Pass Your Handler

Again, you have the option to pass an ErrorHandler object to the static function, ErrorHandler.runApp(), allowing you to specify your own ‘error handler’ and ‘error widget.’ Some sample code demonstrates this below.

void main() {

  ErrorHandler handler = ErrorHandler();
  
  handler.onError = (FlutterErrorDetails details) => yourErrorHandler();
  
  handler.builder = (FlutterErrorDetails details)=> yourErrorWidget();
  
  return ErrorHandler.runApp(MyApp(), handler);
}

Let’s finally take a look at that set() function. Again, it’s in this function, if an ‘error widget’ function is passed to the named parameter, builder, it’s then assigned to ErrorWidget.builder. If an exception handler routine is passed to this function using the named parameter, handler, it’s then assigned to a package-private variable called, _onError. You can see the set() function has its own exception handler routine actually assigned to the static function, Flutter.onError. It in that routine, the variable, _onError, is eventually called. Note, if the variable, _onError, is null, the original exception handler (stored in the variable, _oldOnError) is called instead. Are you following so far? Try it in your favourite debugger and follow along with its execution. You’ll get it.

void set({
  Widget Function(FlutterErrorDetails details) builder,
  void Function(FlutterErrorDetails details) handler,
}) {
  //
  if (builder != null) ErrorWidget.builder = builder;

  if (handler != null) _onError = handler;

  FlutterError.onError = (FlutterErrorDetails details) {
    // Prevent an infinite loop and fall back to the original handler.
    if (_inHandler) {
      if (_onError != null && _oldOnError != null) {
        _onError = null;
        try {
          _oldOnError(details);
        } catch (ex) {
          // intentionally left empty.
        }
      }
      return;
    }

    // If there's an error in the error handler, we want to know about it.
    _inHandler = true;

    final FlutterExceptionHandler handler =
        _onError == null ? _oldOnError : _onError;

    if (handler != null) {
      handler(details);
      _inHandler = false;
    }
  };
}

Set Your Handlers

By the way, there are two setters in the class as well. This allows you another way to assign an error handler routine or ‘error widget’ routine to your app. You can see below that both setters call our old friend, the set() function. Each simply uses the appropriately named parameter. Note, explicitly assigning a null value, will revert each to the original routine. Why? So you have that option. That’s why.

/// Set to null to use the 'old' handler.
set onError(FlutterExceptionHandler handler) {
  // So you can assign null and use the original error routine.
  _onError = handler;
  set(handler: handler);
}

/// Set the ErrorWidget.builder
/// If assigned null, use the 'old' builder.
set builder(ErrorWidgetBuilder builder) {
  if (builder == null) builder = _oldBuilder;
  set(builder: builder);
}

Finally, listed near the end of the ErrorHandler class, is the humble little default ‘error widget’ routine. It's not much to look at admittedly. However, grab a copy and change it. Make it better. Make it your own.

Widget _defaultErrorWidget(FlutterErrorDetails details) {
  String message;
  try {
    message = "ERROR\n\n" + details.exception.toString() + "\n\n";

    List<String> stackTrace = details.stack.toString().split("\n");

    int length = stackTrace.length > 5 ? 5 : stackTrace.length;

    for (var i = 0; i < length; i++) {
      message += stackTrace[i] + "\n";
    }
  } catch (e) {
    message = 'Error';
  }

  final Object exception = details.exception;
  return _WidgetError(
      message: message, error: exception is FlutterError ? exception : null);
}

I’ll leave it at that. Again, this little exercise was to merely get you some code to work with. Take it and consider how you want to handle those inevitable errors in your own app. Your users will be forever grateful.

Cheers.

→ Other Stories by Greg Perry

DECODE Flutter on YouTube
Flutter
Programming
Android App Development
iOS App Development
Mobile App Development
Recommended from ReadMedium