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
.
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();
});
},
),
...
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.
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?
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).
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.
Good one bro
Though i’d want you to tell me why you replaced the future builder ???
I mean it seems to have a good error handling mechanism even if we still stick to a central error class
Please just trying to learn
future builder is fine until your app gets larger, its more okay to use a proper state management tool
It would be great if you link the second part here .. No need to approve this but rather update the article .. I have already added a link for the second part on the website field.