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.
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, Future
s aren't necessarily without side-effects and without getting too far into functional programming, let's just say that Dart's Future
s aren't FP-friendly.
That's why the Dartz package has a wrapper for Future
s 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.
- Convert the
Future<Post>
coming from_postService.getOnePost()
into aTask<Post>
. attempt()
to execute the function held inside theTask
. This automatically catches all exceptions and feeds them into the left side ofEither<Object, Post>
. The type returned isTask<Either<Object, Post>>
.run()
converts theTask
back into aFuture
, giving us aFuture<Either<Object, Post>>
that we can work with as usual.then()
is a classic Dart method used when you don't want to use theawait
keyword. There we simply update theEither<Failure, Post> _post
field by calling_setPost()
.
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 ChangeNotifier
s, 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.
Excellent post! Greetings from Paraguay ??
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.
May you explain how you have written the extension? I can’t understand this generic extension. I
Hi, I really love this approach, so thank you for the tutorial!
However, I recently switched my project to null safety, and the mapLeftToFailure extension now returns a type of Either
Do you have an idea why this happen and how I could cast the Right value to the correct type?
And thanks a lot for your tutorials 🙂
try catch and throw is a bad code smell as one can in fact do Either Right fold for it.
and Effective Dart states use rethrow and throw will swallow the stacktrace defeating your purp[ose in the first place.
try catch and throw are only made for known errors that you don’t need to analyse the stacktrace.
Unknown errors will keep the usual way
Hello, i am testing my implementation of the extension “mapLeftToFailure”. I know my flutter project is not the same version as the video, but the returning value of the extension method is supposed to be Either but mine is returning Either. I don’t know if is the flutter, dart or dartz version, but is this happening to any of you ? Can you help me?
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.