Functional Error Handling in Flutter & Dart (#2 – Either, Task, FP)

4  comments

From the first part, you already know that you shouldn't catch every exception with a blanket catch statement and you also learned how to get error messages over to your state management solution, such as a ChangeNotifer. Some may say "That's it!", but as proper software developers we should prefer hard rules over conventions.

Currently, we won't get any compile-time or even run-time errors when we don't display an error message to the user. Secondly, we treat error flow and "success data" flow separately - one through exceptions, another through return values. Similarly, we store the "success data" and its associated Failure in two distinct fields in the ChangeNotifier... Is there a way to join them?

Introducing functional programming

Although the code we currently have is working fine, there's a way to make it more robust. Functional programming is known for its terseness and reliability. Algebraic data types (a.k.a sealed classes or tagged unions) can be used to reduce error-prone boilerplate to the minimum.

Languages like Scala, Haskell, F# and to an extent even Kotlin support functional programming paradigms out of the box. For Dart, thankfully, there's an amazing package called Dartz. It introduces types like Either and Task which make handling even asynchronous errors a breeze.

Let's add dartz to our project. It's in active development, but the features we use in this tutorial shouldn't undergo any changes.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  provider: ^3.2.0
  dartz: ^0.9.0-dev.5

What needs to change

The code we have inside the PostService class is perfect. To recap, we catch only certain types of exceptions (letting unexpected ones go through) and we throw them converted to Failure objects.

post_service.dart

// This code is great 👍
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 👎");
    }
  }
}

What's not perfect and what we want to change is the PostChangeNotifier and subsequently the UI code responsible for displaying either the success or failure text.

post_change_notifier.dart

...
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);
}
...

main.dart

...
if (notifier.failure != null) {
  return StyledText(notifier.failure.toString());
} else {
  return StyledText(notifier.post.toString());
}
...

Let's go through the needed changes bit by bit, starting at the PostChangeNotifier. At the end we'll have a clean error handling in place.

A Post or a Failure? 🤔

I would argue that there's no place for a separate field for a Post and for a Failure. Logically, we cannot display a post when a failure has occured and vice versa. When you put it this way, there should be only a single field representing both types at the same time.

What's described above is shortly called an improperly modeled domain. It again stems from relying on best practices (you have to remember to check for a failure) instead of relying on the compiler (you cannot ignore to check for the failure if you want the code to compile.

This can be achieved with Either<Failure, Post>. Let's write the first iteration of PostChangeNotifier in a more functional style by practically merging the post and failure fields into one.

post_change_notifier.dart

Either<Failure, Post> _post;
Either<Failure, Post> get post => _post;
void _setPost(Either<Failure, Post> post) {
  _post = post;
  notifyListeners();
}

Of course, this has just broken the rest of the code. Let's first see the immediate benefit of unifying these two types by rewriting the UI code.

Folding Either

To get either the left side or the right side out of an Either type, we use a fold function. Although the old code is now broken, we're currently using an if statement to decide whether to display a failure message or the actual post data.

main.dart

if (notifier.failure != null) {
  return StyledText(notifier.failure.toString());
} else {
  return StyledText(notifier.post.toString());
}

With fold, it's impossible to forget to handle the failure case. In fact, it's more effort to not handle it. That's because fold takes in precisely 2 arguments. And you know what happens when you leave out an argument - a compile time error 🎉

main.dart

return notifier.post.fold(
  (failure) => StyledText(failure.toString()),
  (post) => StyledText(post.toString()),
);

It's nice that this new post field can now hold both a Post and a Failure and that it makes the UI code more robust... But how do we actually get those things in there??? If you've seen my clean architecture & TDD course, we populated the Either type pretty naively. In this tutorial, we're going to utilize functional programming to the fullest.

Task

Dart has a concept of a Future and no doubt, it's a huge help for just about any Dart developer. However, Futures aren't necessarily without side-effects and without getting too far into functional programming, let's just say that Dart's Futures aren't FP-friendly.

That's why the Dartz package has a wrapper for Futures called Task. With it, you can set up a chain of functions which should be executed.

Let's again go bit by bit and start in the simplest possible way. Inside getOnePost, we will replace the try-catch block with the following. I'll break it down in a short while.

post_change_notifier.dart

void getOnePost() async {
  _setState(NotifierState.loading);
  await Task(() => _postService.getOnePost())
      // Automatically catches exceptions
      .attempt()
      // Converts Task back into a Future
      .run()
      // Classic Future continuation
      .then((value) => _setPost(value));
  _setState(NotifierState.loaded);
}

There are four steps in the code above that you probably haven't seen before.

  1. Convert the Future<Post> coming from _postService.getOnePost() into a Task<Post>.
  2. attempt() to execute the function held inside the Task. This automatically catches all exceptions and feeds them into the left side of Either<Object, Post>. The type returned is Task<Either<Object, Post>>.
  3. run() converts the Task back into a Future, giving us a Future<Either<Object, Post>> that we can work with as usual.
  4. then() is a classic Dart method used when you don't want to use the await keyword. There we simply update the Either<Failure, Post> _post field by calling _setPost().
The await keyword waits for the bottom-most Future outputted by then. This way, we can change the state to loading or loaded to show a progress indicator in the UI.

If you've carefully read the steps above, you'll notice that attempt() catches all exceptions whereas in the previous part, you learned why that's not a good practice. Another discrepancy is that attempt() and subsequently run() outputs a Either<Object, Post> while we want to have an Either<Failure, Post>.

These shortcomings will result in crashing the app no matter if we simulate an error by uncommenting a line throwing an exception in post_service.dart or even if we return a successful Post from there.

When _postService.getOnePost() completes successfully, the app crashes with the following message:

_TypeError (type 'Right<dynamic, Post>' is not a subtype of type 'Either<Failure, Post>')

On the other hand, when we throw a Failure from the PostService, the app crashes again with a very similar message:

_TypeError (type 'Left<Object, Post>' is not a subtype of type 'Either<Failure, Post>')

The issue is not in the types being Left and Right instead of Either because Left and Right inherit from Either. The problems lies in the left side holding dynamic or Object while it should hold a Failure.

Casting an Object to a Failure

Casting a simple object to another type is as easy as writing myObject as Failure and you're done! But how do you cast an Object held inside a Task<Either<Object, Post>>? The answer is to map over it, similar to how you can map over a regular Dart List to cast its individual elements.

post_change_notifier.dart

await Task(() => _postService.getOnePost())
    .attempt()
    // Grab the inner 'SomeType' held inside a Task<SomeType>
    // In this case, we get Either<Object, Post>
    .map(
      // Grab only the *left* side of Either<Object, Post>
      (either) => either.leftMap((obj) {
        // Cast the Object into a Failure
        return obj as Failure;
      }),
    )
    .run()
    .then((value) => _setPost(value));

Awesome! The whole map function will return a properly casted Either<Failure, Post>. When you run the app now, it's going to work just as before.

Propagating unexpected errors properly

Now, even though the attempt() function catches all the thrown exceptions, the app will still crash when an unexpected exception (something other than a Failure) arrives. Why? Well, we cast the left side of Either to a Failure. As you know, improper casts result in a CastError.

For the sake of an example, let's fake an unexpected exception in the FakeHttpClient:

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';
    throw FileSystemException();
    return '{"userId":1,"id":1,"title":"nice title","body":"cool body"}';
  }
}

Trying to get a post will now result in the following crash message:

_CastError (type 'FileSystemException' is not a subtype of type 'Failure' in type cast)

Does it make sense though? Sure, we can infer what's going on in the background but wouldn't it be better to simply rethrow the FileSystemException itself? This way, you'll be able to see all of the helpful stack traces when you use a service like Firebase Crashlytics.

To do that, we can add a standard try-catch block to the leftMap function. The only exception that can occur there is a CastError in which case we want to throw the original exception to crash the app.

post_change_notifier.dart

await Task(() => _postService.getOnePost())
    .attempt()
    // Grab the inner 'SomeType' held inside a Task<SomeType>
    // In this case, we get Either<Object, Post>
    .map(
      // Grab only the *left* side of Either<Object, Post>
      (either) => either.leftMap((obj) {
        try {
          // Cast the Object into a Failure
          return obj as Failure;
        } catch (e) {
          // 'rethrow' the original exception
          throw obj;
        }
      }),
    )
    .run()
    .then((value) => _setPost(value));

Pressing the button will now result in a proper app crash:

FileSystemException (FileSystemException: )

Since you'll probably use the above implementation of map throughout your app to handle errors in multiple ChangeNotifiers, I'd say it would be awesome to hide it behind an extension method.

Keeping it short with extensions 🔌

Flutter 1.12 STABLE is here as of December 11th 2019 and together with it comes Dart 2.7 with the support for extensions. I have a separate tutorial about them so I won't explain it here in detail. When we cut & paste the map function to an extension on a Task called mapLeftToFailure():

post_change_notifier.dart

extension TaskX<T extends Either<Object, U>, U> on Task<T> {
  Task<Either<Failure, U>> mapLeftToFailure() {
    return this.map(
      (either) => either.leftMap((obj) {
        try {
          return obj as Failure;
        } catch (e) {
          throw obj;
        }
      }),
    );
  }
}

We can then shorten the getOnePost() method to be just a few lines long. How cool is that?

post_change_notifier.dart

void getOnePost() async {
  _setState(NotifierState.loading);
  await Task(() => _postService.getOnePost())
      .attempt()
      .mapLeftToFailure()
      .run()
      .then((value) => _setPost(value));
  _setState(NotifierState.loaded);
}

In this two-part tutorial you learned how to handle errors properly in you Flutter apps. Especially in this second part, i may seem there's a lot of new and weird code to write, but once you get used to this, there's no coming back 😉. Also, most of the code is just a one-time setup and then you can use it throughout your app without even thinking about how its implemented.

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

Flutter Custom & Staggered Page Transition Animation Tutorial

Flutter Firebase & DDD Course [5] – Sign-In Form Logic

  • I get the idea, why forcing me to handle Failures is a good thing. But doing it in the shown construction – Wrap in Task, attempt, map, run, then… Somehow, what’s missing here is the explanation, why exactly this additonal boiler plating is neccesary. I don’t get it.

    • This code structure is the authors way to ensure that the failures will be handled and the unexpected errors will be informative and not ‘_CastError’. If you think that all this could be simplified somehow (an unmentioned framework maybe?), please feel free to share your thoughts and references with us.

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