A Pragmatic Approach to Error handling
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:
- Errors from which your software can recover
- 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:
- Converting an empty string to an integer
- Using a
late
variable before initialising it. - 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:
- 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.
- 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:
- 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.
- 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:
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.
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 😃:
- Errors that can be recovered should use the explicit error handling technique.
- Errors that cannot be recovered should use the implicit error handling technique.
- Dart doesn’t have the concept of explicit or checked Exceptions like Java.
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