Handling network errors

In my application, I’m making a network request and trying to handle network errors. I tried:

import 'package:http/http.dart' as http;

// other code omitted

  Future<http.Response> postData(String url, Map data) async {
    // Setting up body and headers code omitted

    final response = await http.post(
      Uri.parse(url),
      body: body,
      headers: headers,
    );
    return response;
  }

  void postToServer(BuildContext context) {
    // Code to set up data to post omitted

    if (data.isNotEmpty) {
      try {
        postData(url, data).then((http.Response resp) {
          var s = resp.statusCode < 400 ? 'succeeded' : 'failed';
          if (context.mounted) {
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Update $s.')));
          }
        });
      }
      catch (e) {
        // print(e);
        if (context.mounted) {
          ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Possible network failure. Try later.')));
        }
      }
    }

The code works fine when the network is available. If I temporarily disable networking, I would have hoped to hit the code in the catch clause when the POST request is made, but that isn’t happening. When debugging in VS Code, ClientException with SocketException exception breakpoints are hit (as expected), but control never passes to the exception handling code … how do I achieve that?

How long are you waiting after disabling the network? You will probably find that the specific behaviour or class of exception varies depending on platform and whether your app has made successful calls within the session. On iOS I think it can be up to 60 seconds before an exception is thrown if connectivity is lost partway through an app session.

How long are you waiting after disabling the network?

Long enough for exceptions to be thrown - in the debugger, exceptions are hit as expected inside framework code, but not propagating to my catch clause. This was observed on an app deployed as a desktop app on Linux … but will eventually plan to deploy it to Android, too. The failure happens during DNS lookup (as expected),.

Some tips:

  1. Don’t mix UI code with infrastructure. It’s a recipe for doom.
  2. Don’t use .then. If something is async, use await. Catch will work.

Try this:

 Future<void> postToServer(BuildContext context) async {
    // Code to set up data to post omitted

    if (data.isNotEmpty) {
      try {
        final resp = await postData(url, data);
        final s = resp.statusCode < 400 ? 'succeeded' : 'failed';

          if (context.mounted) {
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Update $s.')));
          }
        }
      }
      catch (e) {
        // print(e);
        if (context.mounted) {
          ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Possible network failure. Try later.')));
        }
      }
    }

You don’t need to await postToServer. You just need to await where you actually care for the response.

If that does not suit your needs, you can always use .onError along with .then.

A future has 3 completion methods: then, onError and whenComplete. The first is what usually goes between try {}. onError is the catch(ex){}. whenComplete is the finally {}.

2 Likes

Try this:

Yes, that did the trick! Thank you very much.

Are you referring to the snackbar code in postToServer here? Perhaps postToServer can return e.g. a Future<String> and that then gets displayed in a snackbar by the UI code that calls postToServer from a button press?

Projects are a lot easier if you separate 3 things:

  1. The code that do I/O. This I/O can be databases, GPSs, files, HTTP requests, etc. Those things that are not possible to test in a unit test or that you could change in your project (for instance: Firebase Auth is wonderful for common apps, but it will not work for a corporate/business app (because they need company authentication). Imagine if your whole authentication stack is a plugin, where you can unplug Firebase Authentication and plug in, for example, Zitadel Authentication. NOTHING in your app would change just because you exchange that plugin).

  2. Your domain. The code that asks questions or command your application to do stuff, in a language appropriate for your domain (i.e.: if you are making an Uber application, your domain is those high-level things like drivers, cars, charges, etc. Your app speaks the language of the solution your are building, not databases, apis, etc. that’s tech stuff that nobody (should) care).

  3. Your view (that’s Flutter). It doesn’t make any sense for your views to change because 1) or 2) above changed (of course, unless it’s a feature that requires UI changes).

The more protected those layers are, the less change your app have, the less bugs your app have.

And, if you are into those things, 2) is a wonderful joy to be unit tested.

  1. It’s all about read documentation for Firebase Auth, Zitadel, Drift, Dio, etc. It’s not your problem, you just use those things.

  2. It’s all about your app, so you can test however you want. If you test all cases, logically your app won’t have any bugs

  3. The view is like 1), it’s all about reading Flutter documentation. Since the source of data is 2), you can change your views whatever you like, optimizing a little bit here and there.

Ok. In theory, it’s nice. How about practice?

  1. Run like a vampire runs from garlic from things like BLoC, Riverpod, etc. Those are cancer. It only adds complexity to your project and hinders your ability to be free to do things the way you wanted.

  2. MVVM isn’t a good fit for Flutter because Flutter don’t have data bindings (where a widget can be bound to a property of a View Model and, when that changes, that widget automatically updates itself). That’s not how Flutter works (it’s similar, but not quite the same). That being said, the best fit for Flutter is the old plain reliable MVC (© 1979)

  3. For your business class, what I like to do (and this is only my personal preference) is to use Mediator pattern - Wikipedia. This allows me to write code as questions (what is the current-authenticated user? What is the current list of products available for purchase?) or orders (do authenticate the user using Google Sign In, add this product to the shopping cart). And have those parts communicate with events (a product has being bought, the user X has signed in). For that, I use this: streamline | Flutter package (check the example app and see if that makes sense for you). I like this particular package because it has a feature I’m requiring right now: the ability to ask simultaneously to many parts of my application if they have some event for a specific day in a calendar (it’s like microservices inside your app). And pipelines. Very good for logging performance with Firebase Performance.

It is more work? Yes, it is. It is worth? Yes, hell, it is. It saves A LOT of headache when the app grows. It is worth for simple apps? Yes, because you should do this stuff as a rule. IMO, programming is not writing code, is using a method, a style to solve problems using code. Is the same as a painter style: the final result is a paint, but the way you achieve that is a style on your own. So, you need to practice your style for anything you write, always.

2 Likes

I recommend my latest article on architecture Flutter Architecture Simplified

Also it worth checking out flutter_command because of the way it encapsulates error handling so commands are really nice to do network calls.

You might want to take a look at the new official documentation on architecture I think it would be a really good fit for you.

From docs.flutter.dev: And if you squint, aren’t all architectures MVVM anyway?