Catch error flutter

How to control error messages and logging of errors
Contents
  • Errors caught by Flutter
  • Define a custom error widget for build phase errors
  • Errors not caught by Flutter
  • Handling all types of errors

The Flutter framework catches errors that occur during callbacks
triggered by the framework itself, including errors encountered
during the build, layout, and paint phases. Errors that don’t occur
within Flutter’s callbacks can’t be caught by the framework,
but you can handle them by setting up an error handler on the
PlatformDispatcher.

All errors caught by Flutter are routed to the
FlutterError.onError handler. By default,
this calls FlutterError.presentError,
which dumps the error to the device logs.
When running from an IDE, the inspector overrides this
behavior so that errors can also be routed to the IDE’s
console, allowing you to inspect the
objects mentioned in the message.

When an error occurs during the build phase,
the ErrorWidget.builder callback is
invoked to build the widget that is used
instead of the one that failed. By default,
in debug mode this shows an error message in red,
and in release mode this shows a gray background.

When errors occur without a Flutter callback on the call stack,
they are handled by the PlatformDispatcher’s error callback. By default,
this only prints errors and does nothing else.

You can customize these behaviors,
typically by setting them to values in
your void main() function.

Below each error type handling is explained. At the bottom
there’s a code snippet which handles all types of errors. Even
though you can just copy-paste the snippet, we recommend you
to first get acquainted with each of the error types.

Errors caught by Flutter

For example, to make your application quit immediately any time an
error is caught by Flutter in release mode, you could use the
following handler:

import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    if (kReleaseMode) exit(1);
  };
  runApp(const MyApp());
}

// rest of `flutter create` code...

This handler can also be used to report errors to a logging service.
For more details, see our cookbook chapter for
reporting errors to a service.

To define a customized error widget that displays whenever
the builder fails to build a widget, use MaterialApp.builder.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, widget) {
        Widget error = const Text('...rendering error...');
        if (widget is Scaffold || widget is Navigator) {
          error = Scaffold(body: Center(child: error));
        }
        ErrorWidget.builder = (errorDetails) => error;
        if (widget != null) return widget;
        throw ('widget is null');
      },
    );
  }
}

Errors not caught by Flutter

Consider an onPressed callback that invokes an asynchronous function,
such as MethodChannel.invokeMethod (or pretty much any plugin).
For example:

OutlinedButton(
  child: const Text('Click me!'),
  onPressed: () async {
    const channel = MethodChannel('crashy-custom-channel')
    await channel.invokeMethod('blah')
  },
)

If invokeMethod throws an error, it won’t be forwarded to FlutterError.onError.
Instead, it’s forwarded to the PlatformDispatcher.

To catch such an error, use PlatformDispatcher.instance.onError.

import 'package:flutter/material.dart';
import 'dart:ui';

void main() {
  MyBackend myBackend = MyBackend();
  PlatformDispatcher.instance.onError = (error, stack) {
    myBackend.sendError(error, stack);
    return true;
  };
  runApp(const MyApp());
}

Handling all types of errors

Say you want to exit application on any exception and to display
a custom error widget whenever a widget building fails — you can base
your errors handling on next code snippet:

import 'package:flutter/material.dart';
import 'dart:ui';

Future<void> main() async {
  await myErrorsHandler.initialize();
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    myErrorsHandler.onErrorDetails(details);
  };
  PlatformDispatcher.instance.onError = (error, stack) {
    myErrorsHandler.onError(error, stack);
    return true;
  };
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, widget) {
        Widget error = const Text('...rendering error...');
        if (widget is Scaffold || widget is Navigator) {
          error = Scaffold(body: Center(child: error));
        }
        ErrorWidget.builder = (errorDetails) => error;
        if (widget != null) return widget;
        throw ('widget is null');
      },
    );
  }
}

When we run a Flutter app, many things can go wrong.

The user may enter an incorrect input, a network request may fail, or we could have made a programmer mistake somewhere, and our app will crash.

Exception handling is a way of dealing with these potential errors in our code so our app can gracefully recover from them.

This article will review the basics of exception handling in Dart and Flutter (using try and catch) and explore how the Result type can help us leverage the type system to handle errors more explicitly.

And in the next articles, we’ll tackle more complex use cases where we need to run multiple asynchronous calls sequentially.

Ready? Let’s go!

sponsor

Code with Andrea is free for everyone. Help me keep it that way by checking out this sponsor:

Cut your code-review time by 40% for free!

Cut your code-review time by 40% for free! gitStream is the free dev tool that automates away the pull-request & code-review problems plaguing cycle time in over 90% of engineering teams.

Exception Handling in Dart and Flutter: The basics

As an example, here’s a simple Dart function that we can use to fetch a location from an IP address:

// get the location for a given IP using the http package Future<Location> getLocationFromIP(String ipAddress) async { try { final uri = Uri.parse('http://ip-api.com/json/$ipAddress'); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); return Location.fromMap(data); default: throw Exception(response.reasonPhrase); } } on SocketException catch (_) { // make it explicit that a SocketException will be thrown if the network connection fails rethrow; } }

In the code above, we’re using the http package to make a GET request to an external API.

If the request is successful, we parse the response body as JSON and return a Location object.

But if the request fails (for example, if the IP address is invalid or the API is down), we throw an exception.

We also wrap our code in a try/catch block to catch any SocketExceptions that might occur if there is a network connection error.

And if we want to use our function, we can simply call it like this:

final location = await getLocationFromIP('122.1.4.122'); print(location);

But hang on there! If the function throws, we’re going to get an unhandled exception.

To fix this, we need to wrap it in a try/catch block:

try { final location = await getLocationFromIP('122.1.4.122'); print(location); } catch (e) { // TODO: handle exception, for example by showing an alert to the user }

Now our code is more robust. But it was too easy to forget to add the try/catch block in the first place.

And that’s because the signature of our function doesn’t make it explicit that it can throw an exception:

Future<Location> getLocationFromIP(String ipAddress)

In fact, the only way to find out if the function throws is to read its documentation and implementation.

And if we have a large codebase, it can be even harder to figure out which functions might throw and which don’t.

Improved Exception Handling with the Result type

What we really want is a way to make it explicit that the function can return a result that can be either success or an error.

Languages such as Kotlin and Swift define their own Result type using language features known as sealed classes (Kotlin) or enums with associated values (Swift).

But in Dart, these features are unavailable, and we don’t have a built-in Result type.

And if we want, we can build our own using abstract classes and generics.

Or we can make our life easy and use the multiple_result package, which gives us a Result type that we can use to specify Success and Error types.

As an alternative to multiple_result, you can use packages such as fpdart and dartz, which have an equivalent type called Either. There is also an official async package from the Dart team. All these packages use a slightly different syntax, but the concepts are the same.

Here’s how we can convert our previous example to use it:

// 1. change the return type Future<Result<Exception, Location>> getLocationFromIP(String ipAddress) async { try { final uri = Uri.parse('http://ip-api.com/json/$ipAddress'); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); // 2. return Success with the desired value return Success(Location.fromMap(data)); default: // 3. return Error with the desired exception return Error(Exception(response.reasonPhrase)); } } catch (e) { // catch all exceptions (not just SocketException) // 4. return Error here too return Error(e); } }

Now our function signature tells us exactly what the function does:

  • return an Exception if something went wrong
  • return a success value with the resulting Location object if everything went well

As a result, we can update our calling code like so:

final result = await getLocationFromIP('122.1.4.122');

And if we want to handle the result, we can use pattern matching with the when method:

result.when( (exception) => print(exception), // TODO: Handle exception (location) => print(location), // TODO: Do something with location );

This forces us to handle the error case explicitly, as omitting it would be a compiler error.

By using the Result type with pattern matching, we can leverage the Dart type system to our advantage and make sure we always handle errors.

The Result type: Benefits

Here’s what we have learned so far:

  • The Result type lets us explicitly declare success and error types in the signature of a function or method in Dart
  • We can use pattern matching in the calling code to ensure we handle both cases explicitly

These are great benefits, as they make our code more robust and less error-prone.

So we can go ahead and use Result everywhere, right?

A noob dev who just discovered the Result type

A noob dev who just discovered the Result type

Before we go ahead and refactor our entire codebase, let’s dig a bit deeper and figure out when using Result may not be a good idea.

When the Result type doesn’t work well

Here is an example of a method that calls several other async methods internally:

Future<void> placeOrder() async { try { final uid = authRepository.currentUser!.uid; // first await call final cart = await cartRepository.fetchCart(uid); final order = Order.fromCart(userId: uid, cart: cart); // second await call await ordersRepository.addOrder(uid, order); // third await call await cartRepository.setCart(uid, const Cart()); } catch (e) { // TODO: Handle exceptions from any of the methods above } }

If any of the methods above throws an exception, we can catch it in one place and handle it as needed.

But suppose we converted each of the methods above to return a Future<Result>.

In this case, the placeOrder() method would look like this:

Future<Result<Exception, void>> placeOrder() async { final uid = authRepository.currentUser!.uid; // first await call final result = await cartRepository.fetchCart(uid); if (result.isSuccess()) { final order = Order.fromCart(userId: uid, cart: result.getSuccess()); // second await call final result = await ordersRepository.addOrder(uid, order); if (result.isSuccess()) { // third call (await not needed if we return the result) return cartRepository.setCart(uid, const Cart()); } else { return result.getError()!; } } else { return result.getError()!; } }

This code is much harder to read because we have to unwrap each Result object manually and write a lot of control flow logic to handle all the success/error cases.

In comparison, using try/catch makes it much easier to call multiple async methods sequentially (as long as the methods themselves throw exceptions rather than returning a Result).

Can we use Result with multiple async calls?

So when considering if you should convert a method to return a Future<Result>, you could ask yourself if you’re likely to call it in isolation or alongside other async functions.

But it’s hard to decide this for every method you declare. And even if a method is called in isolation today, it may no longer be in the future.

What we really want is a way to capture the result of an asynchronous computation made of multiple async calls that could throw, and wrap it inside a Future<Result>.

And that will be the topic of my next article, which will cover functional error handling in more detail.

Conclusion

Let’s wrap up what we have learned so far.

The multiple_result package gives us a Result type that lets us explicitly declare success and error types in the signature of a function or method in Dart:

// 1. change the return type Future<Result<Exception, Location>> getLocationFromIP(String ipAddress) async { try { final uri = Uri.parse('http://ip-api.com/json/$ipAddress'); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); // 2. return Success with the desired value return Success(Location.fromMap(data)); default: // 3. return Error with the desired exception return Error(Exception(response.reasonPhrase)); } } on SocketException catch (e) { // 4. return Error here too return Error(e); } }

And we can use pattern matching in the calling code to ensure we handle both cases explicitly:

// Use like this: final result = await getLocationFromIP('122.1.4.122'); result.when( (exception) => print(exception), // TODO: Handle exception (location) => print(location), // TODO: Do something with location );

However, we have an open question about how to use Result if we have to call multiple async functions sequentially.

I’ll cover this (and more) in upcoming articles about functional error handling:

  • Functional Error Handling with Either and fpdart in Flutter: An Introduction

I’ll also show you some examples of how to handle errors in a medium-sized eCommerce app, such as the one I’ve covered in my latest course. 👇

New Flutter Course Now Available

I launched a brand new course that covers error handling in great depth, along with other important topics like state management with Riverpod, app architecture, testing, and much more:

If you want to find out the exact type of error you are getting, remove the exception so that all errors are caught, put a breakpoint within the catch, and check the type of error. so in this article, we will go through How to Catch Exception In Flutter

Try with the below code snippet:

void loginUser(String email, String password) async {
  try {
    var user = await _data
      .userLogin(email, password);
    _view.onLoginComplete(user);
      });
  } on FetchDataException catch(e) {
    print('error caught: $e');
    _view.onLoginError();
  }
}

catchError is sometimes a bit tricky to get right. With async/await you can use try/catch like with sync code and it is usually much easier to get right.

To handle errors in an async and await function, use try-catch:

Run the following example to see how to handle an error from an asynchronous function.

Future<void> printOrderMessage() async {
  try {
    var order = await fetchUserOrder();
    print('Awaiting user order...');
    print(order);
  } catch (err) {
    print('Caught error: $err');
  }
}

Future<String> fetchUserOrder() {
  // Imagine that this function is more complex.
  var str = Future.delayed(
      Duration(seconds: 4),
      () => throw 'Cannot locate user order');
  return str;
}

Future<void> main() async {
  await printOrderMessage();
}

Within an async function, you can write try-catch clauses the same way you would in synchronous code.

You can also refer to the code snippet like below:

Future < User > userLogin(email, password) async { try {
  Map body = {
    'username': email,
    'password': password
  };
  http.Response response = await http.post(apiUrl, body: body);
  final responseBody = json.decode(response.body);
  final statusCode = response.statusCode;
  if (statusCode != HTTP_200_OK || responseBody == null) {
    throw new FetchDataException(
      "An error occured : [Status Code : $statusCode]");
   }
  return new User.fromMap(responseBody); }
   catch (e){
    print(e.toString());
}

Conclusion:

Thanks for being with us on a Flutter Journey !!!

So in this article, We have been through How to Catch Exception In Flutter.

Keep Learning !!! Keep Fluttering !!!

Let us know in the comments if you are still facing any problems in Flutter development!! We are here to help you 🙂

Flutter Agency is our portal Platform dedicated to Flutter Technology and Flutter Developers. The portal is full of cool resources from Flutter like Flutter Widget GuideFlutter ProjectsCode libs and etc.

Flutter Agency is one of the most popular online portals dedicated to Flutter Technology and daily thousands of unique visitors come to this portal to enhance their knowledge of Flutter.

An exception is an unexpected issue that occurs when executing a program and disrupts its normal flow. Exceptions can cause the application to terminate abnormally if not handled.

Exceptions are intended to be caught and handled so they do not affect the flow of the program. To handle an exception in code, try..catch..finally blocks should be used.

Why Throw Exceptions in Flutter?

Exceptions can be thrown to convey information to the user about a failure that occurred during the execution of a program. They should contain useful data fields to convey the relevant information. This can help the issue be addressed programmatically.

Exceptions can be instantiated and thrown in Flutter using the throw keyword. The syntax for throwing an exception is:

throw new exception_name()

Flutter Throw Exception Example

Here’s an example that shows how to use the throw keyword in Flutter to throw an exception:

void validate_age(int age) { 
    if(age < 0) { 
        throw new FormatException(); 
    } 
}

In the above example, the validate_age function is used to validate an integer age, which should not be negative in value. If the value is found to be less than 0, a FormatException is thrown.

Handling Exceptions in Flutter

To handle exceptions in Flutter, try..catch..finally blocks can be used to prevent the application from terminating abruptly.

The try block contains the code that might possibly throw an exception. The try block must be followed by on or catch blocks, and an optional finally block.

The catch block is used to catch and handle any exceptions thrown in the try block. To catch specific exceptions, the on keyword can be used instead of catch. The catch block can be left at the bottom to catch other exceptions.

The finally block is optional and is always executed whether any exception is thrown or not. It is executed after the try/on/catch blocks.

Here’s the syntax for using the try..catch..finally blocks to handle an exception

try {
// do something
} on Exception1 {
    // handle exception
} catch (ex2) {
    // handle exception
} finally {
// always executed
}

Flutter Handle Exception Example

Here’s an example that shows how to use the try..catch..finally blocks in Flutter to handle an exception:

void main() {
    int a = 1;
    int b = 0;

    try {
        int result = a ~/ b;
    } catch (ex) {
        print('cannot divide by 0');
    } finally { 
        print('finally block executed'); 
    }
}

In the above example, an exception is thrown due to a division by zero attempt. This exception is caught and handled by the catch block. The finally block is executed after the catch block and the following output is produced:

cannot divide by 0
finally block executed

Track, Analyze and Manage Errors With Rollbar

Managing errors and exceptions in your code is challenging. It can make deploying production code an unnerving experience. Being able to track, analyze, and manage errors in real-time can help you to proceed with more confidence. Rollbar automates error monitoring and triaging, making fixing Flutter errors easier than ever. Sign Up Today!

Are you aware that every Dart method can throw an exception at any time? Many Flutter developers understand exceptions the wrong way because exceptions in Dart are different compared to many other languages. Not only can an exception cause an ugly error, but it can also break the users’ flow. This often results in users disliking your Flutter app or creating bad reviews in the app stores. But don’t worry because we’ve got you covered.

We take a deep dive into when and how exceptions should be handled in Dart and Flutter, and also how to tell the user that something went wrong. This will make your Flutter apps truly exceptional 😉.


When things go wrong…

There are a lot of things that can go wrong inside your app – Your API call fails and returns “HTTP – 404” not found, the GPS sensor is unable to return a location or your app was just unable to parse ‘IamAString’ to an Integer. These are issues you probably encounter almost every day!

And then there are also the exceptions that appear that you might not expect. So we must be ready at any time to handle possible errors.

Error vs. assertions vs. exception – What’s the difference?

In Dart, we have three different types of errors that can occur during the development and execution of your app:

  • Errors in Dart are used to let a consumer of an API or library know that they are using it wrong. That means that errors should not be handled and should crash your app. That’s fine because they only occur in development and inform the developer that something is going in the wrong direction.
  • Assertions are similar to errors and are used for reporting bad states that should never happen. The difference is that asserts are only checked in debug mode. They are completely ignored in production mode.
  • Exceptions are for an expected bad state that may happen at runtime. Because exceptions are expected, you should catch them and handle them appropriately.

In this article, we put the focus on exceptions because they are the last opportunity to handle errors before they arrive at your user.

How to recover

You ended up in a situation where your log is filled with error messages and your Flutter app enters an unusable state or even crashes. We need a way to catch those exceptions before they reach the UI and therefore the user. Like most of the programming languages, Dart has you covered and implements a way to handle those arising errors.

To catch an error, we have to wrap the method that throws the error into a try-block. The try block needs to follow a catch block that has an exception parameter of the type object.

try {
 return api.getBird();
} catch (exception) {
    log(e)
}

Like most programming languages, Dart is also offering us a way to execute some code that is called whether an error occurred or not. For this, we can use the finally block. The finally block is e.g. often used to stop some kind of loading animation that was displayed while the call was done.

try {
 return api.getBird();
} catch (exception) {
    log(e)
} finally {
    // Executed after the try block if no error occured 
    // or after the catch block if an error occured
}

If we are interested in which type of exception was thrown, we can check in the catch block which runtimeType the exception has. There is also an optional stack trace parameter in the catch-block.


As a reminder, a stack trace is the list of method calls that were done before the application encountered the exception.


try {
 return api.getBird();
} catch (exception, stacktrace) {
    if(e is BirdNotFoundException) {
        log('Bird not found');
    }
    else if( e is BirdDoesNotExistException) {
        log(Bird does not exist);
    }
}

But Dart provides us with some syntactic sugar here because we can also directly react to different types of exceptions with the on keyword.

try {
 return api.getBird();
} on BirdNotFoundException {
    log('Bird not found');
} on BirdDoesNotExistException {
    log(Bird does not exist);
}
catch (exception) {
    log('No type exception was executed');
}

If the exception is of type BirdNotFoundException or BirdDoesNotExistException, it will execute its corresponding block.
If one of the typed exceptions were executed, the catch block will not be invoked.

This try-catch block will catch all exceptions nevertheless, the invocation of the method in the try block is a synchronous or an asynchronous call (Future). But for Futures, Dart also provides us with some special syntax that makes handling them a little easier.

With the assumption that the getBird() method looks like this:

Future<Bird> getBird();

We can also just call the method with a try-catch block and all exceptions that occur while calling getBird() will get caught.

try {
 return api.getBird();
} 
catch (exception) {
    log('Not type exception was executed');
}

For Futures we can also use this shorter way:

return api.getBird().catchError((e) => log(Exception was thrown $e));

Throwing exceptions yourself is also straightforward. To throw an exception, just use the throw keyword

throw BirdNotFoundException();

In Dart, it is possible to throw everything. You are even able to throw any class. We recommend not doing it because it makes error handling even harder. In our opinion, it only makes sense to only throw classes that are implementing the Exception interface.


As a nice tip: You can enable the “only_throw_errors” lint rule in your “analysis_options.yaml” to enforce that only classes that implement Exception can be thrown.


If you want to catch an exception but still want to propagate it to the caller, use rethrow because it preserves the stack trace.

try {
 return api.getBird();
} 
catch (exception) {
    log('An error occured $exception');
    rethrow;
}

Custom Exceptions

When building your own Flutter app, you might encounter a situation where you want to throw your own exception. Maybe it is because you want to zip together other exceptions or desire to handle a state where your method cannot behave correctly anymore.

The obvious solution would be to throw Exception('Custom message'). The obvious solution is unfortunately not a good solution and even the Dart docs discourage you from doing it.

Creating instances of Exception directly with Exception(“message”) is discouraged in library code

https://api.dart.dev/be/180360/dart-core/Exception-class.html

The issue with this is that it does not give a precise way to catch the exception. The only way to catch the exception as the caller is to wrap everything in an unspecific try-catch block. But that means that we also catch other exceptions and we cannot differentiate them from the others.

The best way is to create a custom exception. To create your own exception class it should implement the Exception interface and then just throw your instantiated exception class.

class CustomException implements Exception {
   const CustomException() : super();
}

void throwsException() {
   if(isWrong()) {
      throws CustomException();
   }
}

The exception handling in Dart is different in some ways compared to other languages. In Dart, we never know if an exception will be thrown by the called method. Methods don’t declare which exceptions they might throw, and you aren’t required to catch any exceptions. In comparison to other languages like Java, all exceptions in Dart are unchecked exceptions. So you must always be prepared!

But why did Dart choose that behavior in the first place?

Join us as a Flutter Developer!

Why Dart has this “exceptional behavior”

You may wonder now why does Dart not enforce use to catch those exceptions? This is a decision that was made on purpose by the Dart team.

I think if a function’s failure values are so important to be handled that you want static checking for them, then they should be part of the function’s return type and not an exception. If you use sum types or some other mechanism to plumb both success and failure values through the normal return mechanism of the function, then you get all of the nice static checkings you want from checked exceptions.

https://github.com/dart-lang/language/issues/984

But this opens up the question of which exceptions should we handle? And how far should we rely on them or should we just build all the possible failures into our return value?

When to Catch or not to Catch

You might ask yourself – Should I now wrap every function call in a try-catch block? No, don’t do that. Most of the function calls are part of a function-call chain, so for most cases, it is enough to wrap the origin of this chain into a try-catch block. If it helps to recover, it still makes sense to use a try-catch block somewhere in this function-call chain, but this always depends on the situation.

There is this mnemonic: ‘Throw early and catch late’.

As a Flutter developer, it is important that we don’t let exceptions through that will be displayed in an uncontrolled manner to the UI. It is always a good idea to wrap function calls that result in a state change with a try-catch block.

Dart also provides a safety net where we can easily catch exceptions that went through. We can wrap our app or specific part with the runZoneGuarded function. Every exception that is thrown up to this point can be handled or at least logged here. This also helps us to avoid channeling up exceptions to the underlying operating system.

Now we ended up in a catch block – What should we do next?

How to tell your user?

We created a simple decision diagram that helps you to decide what to do:

If an error occurs that cannot be solved by the app automatically, you need help from the user.
Sometimes there are even cases where you have to inform your user that they can’t proceed. In cases like that, we have to give the user an appropriate message.

If something critical happens and you know that the error will not be solved by time (e.g The server is unavailable because of maintenance) you need to guide the user on how they can inform the support and get help.

What is a good error message?

We are at the point where we need to show a message to the user because an error has occurred. This message is extremely important and defines a critical point in your app because your user has to take the extra mile to perform their task. So we have created some guidelines for that, what we expect from a good error message:

  • Clear And Not Ambiguous

The message should give the user a clear message about what and, if important, why something went wrong. Make sure that the message can only be interpreted in the way you mean it.

  • Short and meaningful

Error messages need to be short. The user doesn’t want to read a novel – A short explanation is enough.

  • Avoid technical jargons

Remember: Often times your users don’t have the same technical background as you have. Always write messages that are even understandable for your non-tech grandparents 👵🧓.

  • Be Humble and avoid negative works

This one is probably obvious, but it is always good to avoid negative words, especially in a situation where something bad already happened – So use some positivity.

  • Give the user direction on what to do next

The user needs to know what to do next. A direct action e.g a button that solves the situation is always superior to just an info text that instructs the user on what to do. So always prefer direct actions if possible.

Learn from your mistakes

We all make mistakes but it is important that we learn from them. There is this quote that makes a lot of sense, especially with exception handling.

“Do a mistake once, ok, do it twice, ok, do it thrice: you’re an idiot!”

Please don’t take this as an insult 🙂. It is just extremely important that we are aware of any unexpected errors in our Flutter app because our users might encounter them but instead of reporting them to us, they just uninstall the app.
That’s why it is important to log unexpected errors and export them in a way so that we can access them.

You can always build your own solution, but there are already some awesome solutions out there that should cover most of your requirements and also have first-class support for Flutter.

Here are some solutions that we can recommend because we worked with them:

  • Firebase Crashlitics
  • Sentry.io

Conclusion

As a takeaway, it is important to understand that exceptions should not be used for expected behavior that you encounter frequently. In those cases, try to use union/family types and make those exceptions part of your result object. Your type-system will help you to use the APIs correctly.

But for the rare case or the situation where you don’t have any influence embrace the use of exceptions and don’t hesitate to use a try and catch block. Also, don’t forget to hand off a nice message to your user.

Your users will thank you for handling the errors nicely and giving them useful information in a transparent and open way.


Where to go from here

If you want to take a deep dive into some more Dart or Flutter features, the article about Dart Mixins might be a good read for you:

  • Use Dart Mixins More Often! Here is Why…

If your company needs a developer team to implement a Flutter app, reach out to us at [email protected]. Building cool Flutter apps for other businesses is what we do at QuickBird Studios 💫

Do you search for a job as a Flutter Developer?
Do you want to work with people that care about good software engineering?
Join our team in Munich

Why Have Error & Exception Handling?

Most software systems are complicated and written by a team of people.

Complexity arises from multiple sources:

  • The business domain.
  • The act of writing software.
  • From multiple people working together, each one having different viewpoints.
  • etc

The complexity can result in misunderstandings, errors & exceptions.

This is not the end of the world if the code has good error handling.

  • If you don’t handle your errors & exceptions, your software may act unpredictably, and users may suffer a catastrophic error without knowing it or being able to detect when it happened.
  • If you do handle your errors & exceptions, the user may able to continue using the program even with the error / exception and the developers can find the problems over time and improve the software.

Good error & exception handling should not blind the end user with technical jargon, but it should also provide enough information for the developers to trace down the problem.

Dart can throw Errors & Exceptions when problems occur running a Dart program. When an Error or an Exception occurs, normal flow of the program is disrupted, and the program terminates abnormally.

Errors and Exceptions

Errors

Errors are serious issues that cannot be caught and ‘dealt with’. Non-recoverable.

Examples

  • RangeError – programmatic bug where user is attempting to use an invalid index to retrieve a List element.
  • OutOfMemoryError

Exceptions

Exceptions are less-serious issues that can be caught and ‘dealt with’.  

Recoverable.

Examples

  • FormatException – could not parse a String.

Handling Errors

Trying to handle non-recoverable errors is impossible. How can you catch and just handle an out of memory error?

The best thing to do is to log what happened and where so that the developers can deal with them. The approach to this is to add a handler to the top level of your application, for example Sentry or Catcher.

Further Reading

https://medium.com/flutter-community/handling-flutter-errors-with-catcher-efce74397862

Handling Exceptions

Try to handle these to prevent the application from terminating abruptly. If you want your code to handle exceptions then you need to place it in a ‘try..catch..finally’ block. The finally part is optional.

Finally

Dart also provides a finally block that will always be executed no matter if any exception is thrown or not.

Example Code

void main() {
  try {
    // do something here
  } catch (e) {
    // print exception
    print(e);
  } finally {
    // always executed
    print('I will always be executed!');
  }
}

Catch Exception

The first argument to the catch is the Exception.

Example Code

This code catches the Exception and prints it out.
void main() {
  print('start');
  try {
    int.parse("mark");
  } catch (ex) {
    print(ex);
  }
  print('finish');
}

Example Code Output

start
FormatException: mark
finish

Catch Exception and Stack Trace

The second argument to the catch is the StackTrace.

Strack Trace

A Stack Trace is a list of the method calls that the application was in the middle of when an Exception was thrown. The most useful information is normally shown at the top of StackTraces, so you should always look at them from the ‘top down’. Sometimes this takes a lot of scrolling up!

Example Code

This code catches the Exception and StackTrace.

void main() {
  print('start');
  try {
    int.parse("mark");
  } catch (ex, stacktrace) {
    print(stacktrace);
  }
  print('finish');
}

Example Code Output

start
FormatException: mark
FormatException: mark
    at Object.wrapException (<anonymous>:370:17)
    at Object.int_parse (<anonymous>:1555:15)
    at main (<anonymous>:1702:11)
    at dartMainRunner (<anonymous>:9:5)
    at <anonymous>:2206:7
    at <anonymous>:2192:7
    at dartProgram (<anonymous>:2203:5)
    at <anonymous>:2210:3
    at replaceJavaScript (https://dartpad.dartlang.org/scripts/frame.html:39:17)
    at https://dartpad.dartlang.org/scripts/frame.html:69:7
finish

Catch Specific Exceptions

If you know you want to catch a specific Exception then you can use an ‘on’ instead of a ‘catch’. Consider leaving a ‘catch’ at the bottom to catch other Exceptions.

You can optionally add the ‘catch(e)’ or catch(e, s)’ after if you want the Exception and StackTrace data as arguments.

Example Code

void main() {
  print('start');
  try {
    int.parse("mark");
  } on FormatException{
    print('invalid string');
  } catch (ex,stacktrace) {
    print(stacktrace);
  }
  print('finish');
}

Example Code Output

start
invalid string
finish

Throw Exception

To throw an Exception simply use the ‘throws’ keyword and instantiate the Exception.

Example Code

throw new TooOldForServiceException();

Rethrow Exception

Once you have caught an Exception, you have the option of rethrowing it so that it bubbles up to the next level.  So, you could catch an Exception, log it then rethrow it so it is dealt with at a higher level.

Example Code

void misbehave() {
  try {
    dynamic foo = true;
    print(foo++); // Runtime error
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}
void main() {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e.runtimeType}.');
  }
}

Output

misbehave() partially handled JsNoSuchMethodError.
main() finished handling JsNoSuchMethodError.

Create Custom Exceptions

It is very simple to create your own custom Exception.

Simply implement the Exception interface.

Example Code

class TooOldForServiceException implements Exception {
  Cadet _cadet;
 
  TooOldForServiceException(this._cadet);
 
  toString(){
    return "${_cadet.name} is too old to be in military service.";
  }
}
 
class Cadet {
  String _name;
  int _age;
 
  Cadet(this._name, this._age);
 
  get age{
    return _age;
  }
 
  get name{
    return _name;
  }
 
}
 
void main() {
  print('start');
 
  List<Cadet> cadetList = [
    Cadet("Tom", 21),
    Cadet("Dick", 37),
    Cadet("Harry", 51),
    Cadet("Mark", 52),
  ];
 
  List<Cadet> validCadetList = [];
  for (Cadet cadet in cadetList){
    try {
      validateCadet(cadet);
      validCadetList.add(cadet);
    } on TooOldForServiceException catch(ex) {
      print(ex);
    } // .. other validation exceptions ...  
  }
 
  print('finish: ${validCadetList.length} of ${cadetList.length} cadets are valid.');
}
 
void validateCadet(Cadet cadet){
  if (cadet.age > 50){
    throw new TooOldForServiceException(cadet);
  }
  // .. other validations ...
}

 

Example Code Output

start
Harry is too old to be in military service.
Mark is too old to be in military service.
finish: 2 of 4 cadets are valid.

Red and white no smoking sign

Foreword

Due to the rising popularity of Flutter I feel it’s a good time to review some of the aspects of keeping your code clean and maintainable.

One of the things that developers might not pay so much attention to while building apps is error handling. While it might not be so glamorous it is definitely a key part of your application.

Who wants to use an app which looks unresponsive; having confusing error messages; or downright crashing in every step? With this article I’d like to give some good pointers on how to deal with error handling in flutter.

Set up

Let’s start by making a simple application. (If you want to skip right to the meaty part then check out the “handling errors” section.) 

I will be using the test drive app as a base (https://flutter.dev/docs/get-started/test-drive#create-app) and start building it from there.

As you can see the app doesn’t do much at the moment. Let’s make a bit more exciting by trying to build a simple screen where you enter your phone number and it returns a one time password(OTP)

Let’s start by creating a number input field with a submit button.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

...

class _MyHomePageState extends State<MyHomePage> {
 String _phoneNumber;

 void getOneTimePassword(){}

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text(widget.title),
     ),
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           Container(
             child: TextField(
                 onChanged: (phoneNumber) {
                   setState(() {
                     _phoneNumber = phoneNumber;
                   });
                 },
                 decoration: InputDecoration(hintText: 'Enter a phone number'),
                 inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
                 keyboardType: TextInputType.number),
             width: MediaQuery.of(context).size.width * 0.5,
           ),
           RaisedButton(
             onPressed: () {getOneTimePassword();},
             child: Text('Get Code'),
           ),
         ],
       ),
     ),
   );
 }
}

This looks better. But it still doesn’t have any functionality yet. Let’s change that.

Adding more functionality

We will create a OneTimePasswordService and a mock HttpClient that servers our requests. Oh and we also need a response object that will parse the json string we get from the mock client. Let’s create the following 2 files.

import 'dart:math';
import 'otp_response.dart';

class MockHttpClient {
 var randomGenerator = new Random();

 Future<String> getResponseBody() async {
   await Future.delayed(Duration(milliseconds: 1000));
   return _generateOneTimePassword();
 }

 _generateOneTimePassword() {
   return '{ "verificationCode": "' +
       randomGenerator.nextInt(10).toString() +
       randomGenerator.nextInt(10).toString() +
       randomGenerator.nextInt(10).toString() +
       randomGenerator.nextInt(10).toString() +
       '"}';
 }
}

class OneTimePasswordService {
 final httpClient = MockHttpClient();
 Future<OneTimePasswordResponse> getOneTimePassword(String phoneNumber) async {
   final responseBody = await httpClient.getResponseBody();
   return OneTimePasswordResponse.fromJson(responseBody);
 }
}
import 'dart:convert';
import 'package:flutter/foundation.dart';

class OneTimePasswordResponse {
 final String verificationCode;
 OneTimePasswordResponse({
   @required this.verificationCode,
 });

 static OneTimePasswordResponse fromMap(Map<String, dynamic> map) {
   if (map == null) return null;

   return OneTimePasswordResponse(
     verificationCode: map['verificationCode'],
   );
 }

 static OneTimePasswordResponse fromJson(String source) => fromMap(json.decode(source));

 @override
 String toString() {
   return 'verificationCode: $verificationCode';
 }
}

Let’s also modify our main.dart as well

...
import 'otp_service.dart';
...
class _MyHomePageState extends State<MyHomePage> {
 String _phoneNumber;
 String _oneTimePassword;
 final otpService = OneTimePasswordService();
 void getOneTimePassword() async {
   var oneTimePasswordResponse = await otpService.getOneTimePassword(_phoneNumber);
   setState(() {
     _oneTimePassword = oneTimePasswordResponse.toString();
   });
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
...
             width: MediaQuery.of(context).size.width * 0.5,
           ),
           if(_oneTimePassword != null) Text(_oneTimePassword),
           RaisedButton(
...

Now it’s faintly starting to look like something that actually resembles an application.

In a perfect world, everything works flawlessly and there is no need to worry about errors or bugs that might ruin our day. Sadly we don’t live in a perfect world.

Let’s see what will happen if for some reason there is an error from the HttpClient side.

Since we are using our own Mock client we can just replace 

 return _generateOneTimePassword();

with 

 throw HttpException(‘500’);

in otp_service.dart 

When we try to get the code this time we will be greeted by an error in the console. For the user however nothing will be shown. This is not good because the user might think the application is buggy and broken.

Catching exceptions

Let’s try catching the exception.

...
Future<OneTimePasswordResponse> getOneTimePassword(String phoneNumber) async {
 try {
   final responseBody = await httpClient.getResponseBody();
   return OneTimePasswordResponse.fromJson(responseBody);
 } catch (e) {
   print(e);
 }
}
...

Looks good right?

Well. Not really. We actually didn’t really make it any better since all it does now is that it prints the error message in the console. The user will still have no idea why the app is not working. We should avoid this blanket catching at all costs.

But the problem remains. We have to get the error message to the UI somehow.

Useful widgets

Luckily Flutter has an awesome widget called Futurebuilder just for this purpose.

I won’t go over the details for this but if you want to know more then check out the link:

https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

Let’s modify our main.dart to add the previously mentioned Widget

It will allow us to display the latest snapshot based on the otpResponseFuture response

...
import 'otp_response.dart';
...
class _MyHomePageState extends State<MyHomePage> {
 String _phoneNumber;
 Future<OneTimePasswordResponse> otpResponseFuture;
 final otpService = OneTimePasswordService();

 void getOneTimePassword() async {
   setState(() {
     otpResponseFuture = otpService.getOneTimePassword(_phoneNumber);
   });
 }
...
 width: MediaQuery.of(context).size.width * 0.5,
),
FutureBuilder<OneTimePasswordResponse>(
 future: otpResponseFuture,
 builder: (context, snapshot) {
   if (snapshot.connectionState == ConnectionState.waiting) {
     return CircularProgressIndicator();
   } else if (snapshot.hasError) {
     final error = snapshot.error;
     return Text(error.toString());
   } else if (snapshot.hasData) {
     final response = snapshot.data;
     return Text(response.toString());
   } else {
     return Text('After entering the phone number, press the button below');
   }
 },
),
RaisedButton(
...

We also need to remove the try catch block we added in OneTimePasswordService and let the error propagate to our Futurebuilder.

...
Future<OneTimePasswordResponse> getOneTimePassword(String phoneNumber) async {
final responseBody = await httpClient.getResponseBody();
return OneTimePasswordResponse.fromJson(responseBody);
}

Let’s try it out now!

Success! We can now display the error message. The only problem is that the user still won’t understand what this means. We shouldn’t actually show low-level error messages like that to the user.

Another big problem is that we let every exception propagate and get caught by the Futurebuilder. In some cases it might be better to let the app crash instead.

Customized exceptions

A good way is to catch only a particular set of exceptions and display a message based on those.

For that we are also going to create our own custom VerificationException class to customize our messages.

class VerificationException {
 final String message;

 VerificationException(this.message);

 @override
 String toString() => message;
}

Let’s also add an additional catch block for SocketExceptions. (You can additionally test it by throwing a SocketException instead of HttpException in our Mock client)

...
import 'verification_exception.dart';
...
try {
 final responseBody = await httpClient.getResponseBody();
 return OneTimePasswordResponse.fromJson(responseBody);
}
on SocketException {
 throw VerificationException('No Internet connection');
} on HttpException {
 throw VerificationException("Service is unavailable");
}
...

Looks much more readable for the user now doesn’t it?

There is still one issue we need to address. Namely our FutureBuilder currently still catches all errors and displays them which is bad. We only want to catch our own custom exceptions.

Having more control

Thankfully there is a better solution: ChangeNotifier (https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html)

Let’s create a new file called verification_change_notifier

import 'otp_response.dart';
import 'otp_service.dart';
import 'verification_exception.dart';
import 'package:flutter/cupertino.dart';

enum NotifierState { initial, loading, loaded }

class VerificationChangeNotifier extends ChangeNotifier {
  final _otpService = OneTimePasswordService();

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

  VerificationException _exception;
  VerificationException get exception => _exception;
  void _setVerificationException(VerificationException exception) {
    _exception = exception;
  }

  OneTimePasswordResponse _otpResponse;
  OneTimePasswordResponse get otpResponse => _otpResponse;
  void _setOtpResponse(OneTimePasswordResponse otpResponse) {
    _otpResponse = otpResponse;
  }

  void getOneTimePassword(String phoneNumber) async {
    _setState(NotifierState.loading);
    try {
      final otpResponse = await _otpService.getOneTimePassword(phoneNumber);
      _setOtpResponse(otpResponse);
    } on VerificationException catch (f) {
      _setVerificationException(f);
    }
    _setState(NotifierState.loaded);
  }
}

I feel a little bit of explanation is in order.

First of all we have a NotifierState with 3 values:

initial – This is the UI state for when the screen is initially loaded

loading – This state will display the loading indicator

loaded – Finally this state will display us the result or the error, depending on the response from the client

We also define getters and setters for private fields _state,_otpResponse,_exception

Now whenever we call the getOneTimePassword method it will set the correct state and only when we have our custom exception it will set the exception.

In our main class we will replace our Futurebuilder with a Consumer widget (Don’t forget to add the provider dependency)

...
 width: MediaQuery.of(context).size.width * 0.5,
),
Consumer<VerificationChangeNotifier>(
 builder: (_, notifier, __) {
   if (notifier.state == NotifierState.initial) {
     return Text('After entering the phone number, press the button below');
   } else if (notifier.state == NotifierState.loading) {
     return CircularProgressIndicator();
   } else {
     if (notifier.exception != null) {
       return Text(notifier.exception.toString());
     } else {
       return Text(notifier.otpResponse.toString());
     }
   }
 },
),
RaisedButton(
...

...
dependencies:
 flutter:
   sdk: flutter
 provider: ^3.2.0
...

We will replace our getOneTimePassword method as well and remove otpResponseFuture &otpService since our ChangeNotifier does everything already.

Finally we need to wrap all this into a ChangeNotifierProvider

...
class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData(
       primarySwatch: Colors.blue,
       visualDensity: VisualDensity.adaptivePlatformDensity,
     ),
     home: ChangeNotifierProvider(
       create: (_) => VerificationChangeNotifier(),
       child: MyHomePage(title: 'Flutter Error Handling Demo'),
     ),
   );
 }
}
...
void getOneTimePassword() async {
 Provider.of<VerificationChangeNotifier>(context).getOneTimePassword(_phoneNumber);
}
...

There you have it! Now we have a working app that shows an intelligible error message to the user and also doesn’t catch and show all errors.

Conclusion

Handling errors is nothing you should be afraid of. It will save you a lot of headache later on if you properly manage your errors. The suggestions in this article is only the tip of the iceberg when it comes to error handling but I hope it gave a vague idea how it’s possible to do this in Flutter.

Happy Coding!

Today we will see how we can properly handle errors and exceptions in Flutter.

Watch Video Tutorial


For this example we will be doing a service call and handle exceptions related to that.

We will create a sample service here

 static const String url = 'https://jsonplaceholder.typicode.com/users';

 static Future<List<User>> getUsers() async {
    try {
      final response = await http.get(url);
      if (200 == response.statusCode) {
        List<User> users = parseUsers(response.body);
        return users;
      } else {
        return List<User>();
      }
    } catch (e) {
      throw Exception(e.message);
    }
  }

  static List<User> parseUsers(String responseBody) {
    final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
    return parsed.map<User>((json) => User.fromJson(json)).toList();
  }

// User.dart
class User {
  int id;
  String name;
  String email;

  User({this.id, this.name, this.email});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
}

In the above example we are catching all exceptions using a simple try catch block which is not suitable since there can be a variety of Exceptions in this scenario like a SocketException, HttpException or a FormatException.

So in that case how do we catch those exceptions separately and provide appropriate messages to the user.


Now the beauty of dart is you can throw any custom object as Exception. So lets create some custom classes to throw for each exception above.

class NoInternetException {
  String message;
  NoInternetException(this.message);
}

class NoServiceFoundException {
  String message;
  NoServiceFoundException(this.message);
}

class InvalidFormatException {
  String message;
  InvalidFormatException(this.message);
}

class UnknownException {
  String message;
  UnknownException(this.message);
}

Now we will modify our function to get the users above like below

import 'dart:io';

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'User.dart';
import 'Exceptions.dart';

class Services {
  static const String url = 'https://jsonplaceholder.typicode.com/users';

  static Future<List<User>> getUsers() async {
    try {
      final response = await http.get(url);
      if (200 == response.statusCode) {
        List<User> users = parseUsers(response.body);
        return users;
        //throw Exception('Unknown Error');
      } else {
        return List<User>();
      }
    } on SocketException catch (e) {
      throw NoInternetException('No Internet');
    } on HttpException {
      throw NoServiceFoundException('No Service Found');
    } on FormatException {
      throw InvalidFormatException('Invalid Data Format');
    } catch (e) {
      throw UnknownException(e.message);
    }
  }

  static List<User> parseUsers(String responseBody) {
    final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
    return parsed.map<User>((json) => User.fromJson(json)).toList();
  }
}

In the UI, we will catch the exception like this

 list() {
    return FutureBuilder(
      future: users,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          List<User> users = snapshot.data;
          if (users.isEmpty) {
            return showError('No Users');
          }
          return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: users
                .map(
                  (user) => Padding(
                    padding: EdgeInsets.all(10.0),
                    child: Text(
                      user.name,
                      style: TextStyle(
                        fontSize: 20.0,
                      ),
                    ),
                  ),
                )
                .toList(),
          );
        }
        if (snapshot.hasError) {
          if (snapshot.error is NoInternetException) {
            NoInternetException noInternetException =
                snapshot.error as NoInternetException;
            return showError(noInternetException.message);
          }
          if (snapshot.error is NoServiceFoundException) {
            NoServiceFoundException noServiceFoundException =
                snapshot.error as NoServiceFoundException;
            return showError(noServiceFoundException.message);
          }
          if (snapshot.error is InvalidFormatException) {
            InvalidFormatException invalidFormatException =
                snapshot.error as InvalidFormatException;
            return showError(invalidFormatException.message);
          }
          UnknownException unknownException =
              snapshot.error as UnknownException;
          return showError(unknownException.message);
        }
        return CircularProgressIndicator();
      },
    );
  }

In the above code, we catch each exception accordingly and show the correct error.


Понравилась статья? Поделить с друзьями:
  • Catch error 404
  • Catch console error
  • Catalyst control center не поддерживается версией драйвера включенного видеоадаптера как исправить
  • Cat ошибка 261 13
  • Cat write error no space left on device