A Pragmatic Approach to Error handling

Sagar Suri
Flutter Community
Published in
8 min readJul 3, 2022

--

Any software you deploy will likely face a lot of errors. Users will provide invalid inputs, the server might go down or you leave some tiny little bugs. No matter how well tested your software is, errors are inevitable. Things can and will go wrong. Hence we cannot write robust code without thinking about error cases and how to deal with them.

In today’s article, you will learn about a specific pattern that will ensure that errors don’t go unnoticed and are handled appropriately in your Flutter app.

Note: This is a subjective topic and different developers have different opinions about it. Hence I will be sharing my opinion and approach to error handling in a Flutter app.

Types of Error

When you think about errors, there are two possible types of them:

  1. Errors from which your software can recover
  2. Errors from which your software cannot recover or doesn’t make sense to recover

Recoverable Errors

Many errors occur in your app that are not fatal and have sensible ways to handle and recover from them. For example, if a user provides an invalid email address on your login page you won’t just crash the app but instead, show a nice error message of what went wrong. Generally, most errors that are caused by something external to the app should be handled gracefully e.g network errors, corrupted files or invalid inputs.

Unrecoverable Errors

These type of errors occur when a programmer “screw something up” while implementing a logic. Here are some examples:

  1. Converting an empty string to an integer
  2. Using a late variable before initialising it.
  3. Using () => widget.onClick instead of () => widget.onClick() 😉

If there is no sensible way to recover from an error, then the only way to handle such a case is to fail fast and fail loudly. This maximises the likelihood that an engineer notices and fixes the problem.

Thought Process while writing code

The majority of errors occur when one piece of code interacts with another piece of code. As a developer when you write a function you make as few assumptions as possible about how your function is going to be used. Sometimes you know how your function will be used and can make assumptions about the input or situations in which your code will be executed. For example, you have written a function which parses a hard-coded string into URI. In this example, you know that the hard-coded string is a valid URL and you can safely parse it without having the logic to handle the error.

But if your function will be called from multiple places and you are not sure how your function will be used, you are not in a good position to make assumptions about the exact input or situations in which your code will be executed. You also cannot make a conscious decision whether to recover or not if an error occurs. If you take the previous example regarding parsing URL, instead of a hard-coded URL if the URL is provided by an external system(assume a user). You cannot assume that the external system will always provide a valid URL. You need to have the logic to handle invalid URLs and return an error to the caller instead of just crashing. So that the caller can show a nice UI error to the external system(user).

Now let’s assume you have written the logic to handle invalid inputs(for simplicity only empty URL) provided to your function. Your code looks something like the following:

In Dart, the way you can signal an error to the caller is by throwing an Exception or Error . Now with the above implementation comes a question:

How will the caller know that they need to handle errors thrown by your function?

The return type of the function is just Uri and in Dart we have unchecked Exception . By calling the function the caller will never be hinted to handle an error unless he/she goes through the implementation detail of your function. This can cause the caller to not handle the error properly and lead to a programming error. In the next section, we will talk about a better way of signalling error and what approach to avoid.

Signalling Error

When an error occurs in your program, it’s necessary to signal it to some higher level in the program. You should follow two different strategies of signalling error when it comes to recoverable and non-recoverable errors:

  1. If you can recover from the error then it means signalling to the immediate caller (one or two-level higher in the chain) so that they can handle it gracefully.
  2. If you cannot recover from the error then somewhere at a central place in your program you log the error and abort. e.g logging to Firebase crashlytics.

In this article, I will briefly talk about signalling errors that a caller wants to recover from.

Signalling errors that are recoverable

There are many ways you can signal errors. But broadly speaking, the ways of signalling an error falls down into two categories:

  1. Explicit: The caller of your code is forced to be aware that some error might happen. Now it’s up to the caller to either handle it or pass it to the next caller or completely ignore it. The error is part of the code contract and can never go unnoticed by the caller.
  2. Implicit: The error is not part of the code contract which the caller is going to call. The caller has to actively read the document or the implementation detail to be aware of any error that the code can signal.

In a Dart or Flutter app, the most common way to signal an error is by throwing an Exception . This type of signalling error falls down under implicit type because the caller has to either read the comment if there is any or go through the implementation detail to figure out if any Exception is thrown by this piece of code. Look at the following example:

The caller will never come to whether this function can throw errors when calling it. The error is not part of the code contract.

Either pattern

There is a specific pattern you can use to convert this implicit error signalling to explicit. You can introduce the concept of Either return type. Let’s look at an example:

Either can return your one value at a time out of two possible values. The right type is associated with success and the left type is associated with failure. Either.left will return a failure value and Either.right will return success value.

By following this pattern you make sure that the user of your method can never miss the error scenario. He/She has to make a decision on what to do with the error. Either handle it or pass it to the upper layer who can handle it. Let’s see two different examples one where the user of your method wants to handle the error and another where the user decides to pass it to the upper layer:

user handling the error state

In the above example, the user has decided to change the state of the UI based on the return type i.e either success or error.

user passing the error to the upper layer

In the above example, the getUsers method is not handling the error state and passing it to the upper layer to deal with it. The upper layer could be a presenter which will show some error message to the end-user if there is an exception.

Best practice

Till now you have seen examples which demonstrate scenarios where the exceptions are manually thrown by the developer based on certain conditions. You can easily use Either.left because the exceptions are created by you. But what if you are using an external plugin and are not sure what are the different exceptions the plugin can throw. Let’s take the example of http plugin. Look at the following example:

The above example getUser is trying to fetch user information from the backend. _getUserFromNetwork will make an HTTP get call. Since the HTTP plugin can throw SocketException in case of timeout. You need to handle that scenario and return it to the upper layer in the form of Either type. You should be using TaskEither which is another form of Either but it handles async calls. Also, you should be using the factory constructor tryCatch which will handle any type of exception thrown by the plugin.

Most of the time you should use tryCatch when calling a plugin(dio, hive). This will make sure you have not left out exceptions that are thrown by the plugin.

Summary

Let’s recap some important points before closing out 😃:

  1. Errors that can be recovered should use the explicit error handling technique.
  2. Errors that cannot be recovered should use the implicit error handling technique.
  3. Dart doesn’t have the concept of explicit or checked Exceptions like Java.
  4. Either can be one of the explicit ways of handling recoverable errors.

Next Steps

You can read how Rust and Swift have used the explicit error handling technique. Hope you enjoyed the article. Please do connect with me on Twitter and LinkedIn. Stay safe and stay healthy. 😃

Follow Flutter Community on Twitter: https://www.twitter.com/FlutterComm

--

--

Sagar Suri
Flutter Community

Google certified Android app developer | Flutter Developer | Computer Vision Engineer | Gamer