Proper Error Handling in Flutter & Dart (#1 – Principles)

0  comments

As much as we'd all like to close our eyes and pretend that errors don't exist, we still have to deal with exceptions on a daily basis. Modern languages, including Dart, support exception throwing and catching. However, if you've developed apps for some time you may have become frustrated with this special flow of errors in the program. Is there a way to return errors just like you return regular values while keeping the code maintainable?

This two part tutorial series will show you how to deal with errors properly. In the first part, you're going to learn the best practices of classic error handling, using try-catch blocks. The second part will introduce powerful concepts from functional programming that will make handling errors even more streamlined.

Starting out

You are going to learn proper error handling on a simple app which "fetches" a post from a fake HTTP API. Grab the starter project to get up and running quickly. It contains code which makes an app looking like this:

The starter app consists of a StatefulWidget with a FutureBuilder. This in turn calls a PostService to getOnePost() which returns an instance of the Post model class. This model gets converted from JSON which is supplied from a FakeHttpClient.

For starters, we're using only a simple StatefulWidget for state management. Toward the end of the this part, we will migrate to provider. However, error handling techniques described in this tutorial also apply to mobx, bloc, redux or anything else.

Inside the FakeHttpClient, we can manually simulate 3 errors by uncommenting lines.

post_service.dart

class FakeHttpClient {
  Future<String> getResponseBody() async {
    await Future.delayed(Duration(milliseconds: 500));
    //! No Internet Connection
    // throw SocketException('No Internet');
    //! 404
    // throw HttpException('404');
    //! Invalid JSON (throws FormatException)
    // return 'abcd';
    return '{"userId":1,"id":1,"title":"nice title","body":"cool body"}';
  }
}

The worst error handling 😨

The worst thing you can do is to catch exceptions in "silence" with a blanket catch statement. That's what we're currently doing in the starter project.

post_service.dart

class PostService {
  final httpClient = FakeHttpClient();
  Future<Post> getOnePost() async {
    try {
      final responseBody = await httpClient.getResponseBody();
      return Post.fromJson(responseBody);
    } catch (e) {
      print(e);
    }
  }
}

Catching every exception and just printing it out doesn't solve anything. Honestly, it's better to just let the exception propagate and crash the app. We need to get the error message to the UI / state management object of your choice.

Letting raw exceptions propagate 🚀

It's simple then, right? If we don't catch anything in the PostService, it will have no other choice than to propagate to the UI.

post_service.dart

Future<Post> getOnePost() async {
  final responseBody = await httpClient.getResponseBody();
  return Post.fromJson(responseBody);
}

Since we use a FutureBuilder, displaying the error to the user is extremely simple and the part responsible for this is highlighted below.

main.dart

...
FutureBuilder<Post>(
  future: postFuture,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      final error = snapshot.error;
      return StyledText(error.toString());
    } else if (snapshot.hasData) {
      final post = snapshot.data;
      return StyledText(post.toString());
    } else {
      return StyledText('Press the button 👇');
    }
  },
),
RaisedButton(
  child: Text('Get Post'),
  onPressed: () async {
    setState(() {
      postFuture = postService.getOnePost();
    });
  },
),
...
Although the FutureBuilder catches exceptions internally, you might still get a "crash" while debugging in VS Code. In such a case, uncheck Uncaught exceptions in the Debug tab.

The issue with this simple approach is that we don't want to display low-level error messages to the user. This is what's currently displayed when there's an incorrect JSON string inputted to the Post.fromJson method. Does it tell anything to the user? Not really.

A non-helpful error message for what really is an internal server error.

Another HUGE issue is that we let every exception to be propagated and caught by the FutureBuilder. What if there is some unrecoverable error upon which it's better to let the app crash?

Blanket catch statements tend to do more harm than good. Getting rid of all exceptions should be avoided at all cost.

Customizing error messages ✍

Catching only particular types of exceptions and then customizing error messages is a better option. However, we mustn't let ourselves slip into just printing out the messages, because they will again not be shown in the UI...

post_service.dart

class PostService {
  final httpClient = FakeHttpClient();
  Future<Post> getOnePost() async {
    // Printing is nice, but we want these messages in the UI
    try {
      final responseBody = await httpClient.getResponseBody();
      return Post.fromJson(responseBody);
    } on SocketException {
      print('No Internet connection 😑');
    } on HttpException {
      print("Couldn't find the post 😱");
    } on FormatException {
      print("Bad response format 👎");
    }
  }
}

The trick is to create a single app-wide Failure class used solely for the purpose of getting error messages to the UI / the state management solution of your choice. Basically, instead of printing the error messages, you'll throw a new instance of Failure.

post_service.dart

class PostService {
  final httpClient = FakeHttpClient();
  Future<Post> getOnePost() async {
    try {
      final responseBody = await httpClient.getResponseBody();
      return Post.fromJson(responseBody);
    } on SocketException {
      throw Failure('No Internet connection 😑');
    } on HttpException {
      throw Failure("Couldn't find the post 😱");
    } on FormatException {
      throw Failure("Bad response format 👎");
    }
  }
}

class Failure {
  // Use something like "int code;" if you want to translate error messages
  final String message;

  Failure(this.message);

  @override
  String toString() => message;
}

Moving to a ChangeNotifier

The code above works really well for the simple StatefulWidget + FutureBuilder solution. Having a central app-specific Failure class will work equally well for a more mature state management solution, such as provider + ChangeNotifier or anything else.

Let's create a PostChangeNotifier which will serve as a view model to the UI, making code more maintainable, readable and testable.

post_change_notifier.dart

enum NotifierState { initial, loading, loaded }

class PostChangeNotifier extends ChangeNotifier {
  final _postService = PostService();

  NotifierState _state = NotifierState.initial;
  NotifierState get state => _state;
  void _setState(NotifierState state) {
    _state = state;
    notifyListeners();
  }

  Post _post;
  Post get post => _post;
  void _setPost(Post post) {
    _post = post;
    notifyListeners();
  }

  Failure _failure;
  Failure get failure => _failure;
  void _setFailure(Failure failure) {
    _failure = failure;
    notifyListeners();
  }

  void getOnePost() async {
    _setState(NotifierState.loading);
    try {
      final post = await _postService.getOnePost();
      _setPost(post);
    } on Failure catch (f) {
      _setFailure(f);
    }
    _setState(NotifierState.loaded);
  }
}

As you can see, we store the Failure instance in its own field. Note that we catch only errors of type Failure to let other errors which weren't handled in the PostService crash the app (a.k.a. fatal errors).

With proper state management solutions like change notifier, you might be tempted to not have an app-specific Failure class to which all "catchable" errors are converted in an outside world boundary class like PostService. Instead, you may want to catch exceptions directly in the ChangeNotifier and convert them to messages right there. I would advise against it, as it's best to catch errors closest to their source, not a few layers away. Even error handling should follow the single responsibility principle!

With the notifier set up, we can migrate the UI to use it, replacing the FutureBuilder with a Consumer.

main.dart

...Consumer<PostChangeNotifier>(
  builder: (_, notifier, __) {
    if (notifier.state == NotifierState.initial) {
      return StyledText('Press the button 👇');
    } else if (notifier.state == NotifierState.loading) {
      return CircularProgressIndicator();
    } else {
      if (notifier.failure != null) {
        return StyledText(notifier.failure.toString());
      } else {
        return StyledText(notifier.post.toString());
      }
    }
  },
),
RaisedButton(
  child: Text('Get Post'),
  onPressed: () async {
    Provider.of<PostChangeNotifier>(context).getOnePost();
  },
),
...

Oh, and also don't forget to wrap the Home widget in a ChangeNotifierProvider.

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      home: ChangeNotifierProvider(
        create: (_) => PostChangeNotifier(),
        child: Home(),
      ),
    );
  }
}

What will come in the next part

The code we've written in this first part is good enough. However, we can make it better by introducing a few practices from functional programming.  

Some of the issues which the code currently has are:

  • Will we have to have multiple failure fields as well or will we just throw all errors into a single field?
  • What about the if statement where we're checking if failure isn't null? Wouldn't it be better to be forced to account for a failure than just leaving it to be a good practice?

All of these problems stem from one thing - the flow of errors is different from the flow of "success" data. You'll learn how to join errors and regular data in the next part, which will allow for a very nice error handling, especially as your apps become larger.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter freelancer and most importantly developer educator, he doesn't have a lot of free time 😅 Yet he still manages to squeeze in tough workouts 💪 and guitar 🎸

You may also like

Dio Connectivity Retry Interceptor – Flutter Tutorial

Flutter Firebase & DDD Course [3] – Auth Facade Interface

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
>