Handling Flutter errors with Catcher

Jakub Homlala
Flutter Community
Published in
8 min readFeb 10, 2019
Photo by Chor Hung Tsang on Unsplash

Handling errors is everyday programmer job. This job is done infinite times in daily work. In Dart we can handle it easily with try-catch block. But what will happen if we miss some code? We get for example “red screen of death”.

Typical red screen of death in Flutter.

It’s okay that our code is not always perfect. Every developer do mistakes, but great developer will fix his mistakes.

Great thing is being clear with users. When we’re users of some application, we want to know that something unexpected happend and we can decide to send error logs to developer or not. Error logs will help developers patching errors.

Developers can fix errors, but they need to know what happend and where. In mobile applications development we need tool, that will report invalid application behaviour to the authors. Currently in Flutter we have support for Sentry error tracking and soon to be Firebase Crashlytics support.

What if we don’t want to use Sentry or Crashlytics? What if we could use some generic tool, which won’t be hard to configure and will help catch errors in developoment stage or even in release? A tool that composes email, so user just need to click “Send” or save crash logs in device memory? And here comes Catcher.

Introducing Catcher

Catcher logo.

Catcher is new Flutter plugin which catches and handles errors in Flutter application. Catcher offers multiple report modes and handlers to cooperate with Flutter applications. Catcher is heavily inspired from ACRA.

Catcher report flow is easy to understand (see diagram below). Catcher injects error handlers into your application and catches all unchecked errors. Once it catches error, it creates report and sends it to reporter. Reporter shows information about error and waits for user decision. User can accept or cancel error report. If user accepts error report, then it will be processed by handlers.

You can report your checked errors which were caught in your try catch block.

Catcher also collects information about user device hardware and OS. These data can be accessed without any user permission because there’s no personal informations about user. This data will be very helpful, because sometimes error happen because of device errors, not developer.

Diagram which presents how Catcher works.

Using Catcher

Let’s see a basic example of Catcher. First, we need to install plugin. Go to your pubspec.yaml and add this line:

dependencies:
catcher: ^0.0.8

Next, you need to run packages get command which downloads Catcher and enables it in your project.

Last step is adding this import:

import 'package:catcher/catcher_plugin.dart';

We are ready to use Catcher. Below you can find basic example of Catcher.

main() {
//debug configuration
CatcherOptions debugOptions =
CatcherOptions(DialogReportMode(), [ConsoleHandler()]);
//release configuration
CatcherOptions releaseOptions = CatcherOptions(DialogReportMode(), [
EmailManualHandler(["recipient@email.com"])
]);
//profile configuration
CatcherOptions profileOptions = CatcherOptions(
NotificationReportMode(),
[ConsoleHandler(), ToastHandler()],
handlerTimeout: 10000,
customParameters: {"example": "example_parameter"},
);

//MyApp is root widget
Catcher(MyApp(),
debugConfig: debugOptions,
releaseConfig: releaseOptions,
profileConfig: profileOptions);
}

Normally, when you run your code, your main has only this line: runApp(MyApp()); , which starts application. When you are using Catcher, you won’t specify this line anymore. Instead of this, you need to create Catcher instance with your root widget instance and application profiles.

Catcher allows you to configure 3 application profiles: debug , release and profile . In code above we‘re creating 3 instances of CatcherOptions which describes how Catcher will behave in different modes. Catcher will use debugConfig when the application will run in debug environment, releaseConfig when the application will run in release environment and profileConfig when the application run in ‘profile’ mode.

In each CatcherOptions instance you can configure different report mode and handlers list. Click here to check all configuration parameters of CatcherOptions .

There are 4 report modes:

  • Silent report mode
  • Notification report mode
  • Dialog report mode
  • Page report mode

There are 6 handler types:

  • Console handler
  • Http handler
  • File handler
  • Toast handler
  • Email auto handler
  • Email manual handler

When you’re creating Catcher instance, it will starts your root widget and listens to any errors that happend in your application. Running code above in debug mode, will show dialog once error happens, where user can make decision. Once user accepts report, it will be processed by console handler and that will simply print report in console.

You can find complete basic example here.

Basic example with dialog report mode and console handler. Console shows full report data.

Report modes

Let’s talk about report modes. As you know report mode is the way how we show information about error to user. Let’s see closer to each report mode.

Silent report mode is mode where user don’t take any action. There won’t be any information shown to user about error. User won’t know anything about error, unless some visual handler will show it (for example Toast Handler). Use this report mode if you don’t want to ask users for permissions to handle errors.

Example usage:

CatcherOptions(SilentReportMode(), [ConsoleHandler()]);

Notification report mode shows user local notification. Once user clicks on it, report will be accepted and processed by configured handlers. To dismiss report, user can remove notification with swipe.

Example usage:

CatcherOptions(NotificationReportMode(), [ConsoleHandler()]);

Dialog report mode shows user dialog. There are 2 buttons in this dialog: Accept and Cancel. Click on accept button will push log to handlers, cancel will dismiss report.

Example usage:

CatcherOptions(
DialogReportMode(
titleText: "Title",
descriptionText: "Description",
acceptText: "Accept",
cancelText: "Cancel"),
[ConsoleHandler()]);

Page report mode shows new full screen page with description of error, stack trace and 2 buttons.

Example usage:

CatcherOptions(
PageReportMode(
titleText: "Title",
descriptionText: "Description",
acceptText: "Accept",
cancelText: "Cancel",
showStackTrace: false),
[ConsoleHandler()]);

Dialog report mode and page report mode requires navigation key to be configured in your application. This is very easy and you just need to add one line in your MaterialApp widget.

@override
Widget build(BuildContext context) {
return MaterialApp(
//********************************************
navigatorKey: Catcher.navigatorKey,
//********************************************
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: ChildWidget()),
);
}

Report modes have multiple configuration options. You can find all of them here.

Report modes (from left): Notification, Dialog, Page. Silent report mode doesn’t have any visuals.

Handlers

Handlers are last element in report flow. They consume report and do something with him. You can configure multiple handlers in each profile, for example Console handler, File Handler. Let’s see every handler closer.

Console handler is basic handler. It will print formatted report to console. You can configure console handler to print or not some parts of report.

Example usage:

CatcherOptions(DialogReportMode(), [
ConsoleHandler(
enableApplicationParameters: true,
enableCustomParameters: true,
enableStackTrace: true,
enableDeviceParameters: true)
]);
Console handler.

File handler can be used to store logs in user device. You just need to pass file where you want to store your logs.

Example usage:

Directory externalDir = await getExternalStorageDirectory();
String path = externalDir.path.toString() + "/log.txt";

CatcherOptions debugOptions = CatcherOptions(DialogReportMode(),[FileHandler(File(path))]);
CatcherOptions releaseOptions = CatcherOptions(DialogReportMode(),[FileHandler(File(path))]);

Catcher(MyApp(), debugConfig: debugOptions, releaseConfig: releaseOptions);
File handler logged report in file.

Http Handler allows to send data via Http request to server. Currently only Http POST request is supported. You can add custom headers to your request.

Example usage:

CatcherOptions(DialogReportMode(), [
HttpHandler(HttpRequestType.post, Uri.parse("http://logs.server.com"),
headers: {"header": "value"}, requestTimeout: 4000, printLogs: false)
]);

There is simple backend server in Java which exposes endpoint for Catcher reports. You can find implementation here.

Backend server shows collected reports.

Email auto handler adds email feature. This handler with automatically send emails to specified email address. You need to specify username and password of your sender email account, so because of this, I recommend use of this handler only during development stage.

Example usage:

CatcherOptions(DialogReportMode(),
[EmailAutoHandler(
"smtp.gmail.com", 587, "somefakeemail@gmail.com", "Catcher",
"FakePassword", ["myemail@gmail.com"])
]);
Email received from email auto handler.

Email manual handler is different from Email auto handler. This handler creates email and opens default email application. User need to take action to send email. You don’t need to specify sender username and password, because user will use their email as a sender, so it’s safe to use in release stage.

Example usage:

CatcherOptions(DialogReportMode(), [
EmailManualHandler(["email1@email.com", "email2@email.com"],
enableDeviceParameters: true,
enableStackTrace: true,
enableCustomParameters: true,
enableApplicationParameters: true,
sendHtml: true,
emailTitle: "Sample Title",
emailHeader: "Sample Header",
printLogs: true)
]);
Example email composed by Email manual handler..

Toast handler is the last handler. It shows toast in user screen. This is great to use when you just need to show short information to user.

Example:

CatcherOptions(DialogReportMode(), [
ToastHandler(
gravity: ToastHandlerGravity.bottom,
length: ToastHandlerLength.long,
backgroundColor: Colors.red,
textColor: Colors.white,
textSize: 12.0,
customMessage: "We are sorry but unexpected error occured.")
]);
Toast handler shows toast with information about error.

Description of all handler and their options can be find here.

You can even define your own handler! Just create new class and extend ReportHandler class:

import 'package:catcher/catcher_plugin.dart';

class MyHandler extends ReportHandler{
@override
Future<bool> handle(Report error) async{
//my implementation
return true;
}

}

Conclusion

Catcher is new plugin but yet powerful. Plugin is still in development, but you can use it in your project. It’s pretty easy and straightforward to implement it in your project, so give a try!

Feel free to report issues or provide feedback in Github. You are also welcome to add new features to Catcher. Feel free to contribute to this project!

Thanks for reading!

--

--