Handling Exceptions in Dart & Flutter: Everything you need to know

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:

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:

Thanks for reading! If you enjoyed reading the article, please support us by sharing it. And if you have any questions or ideas, don’t hesitate to get in touch with us on Twitter!

Join us as a Flutter Developer!

🐣

Get notified when our next article is born!

(no spam, just one app-development-related article per month)