In mobile development, error handling is a crucial aspect that we cannot afford to overlook. After all, any unhandled error can result in a crash, directly impacting the user experience. Additionally, not knowing where and why an error occurs can be extremely frustrating for developers.
In this article, I will demonstrate how a simple pattern can help us handle potential errors effectively, "forcing" us to address them properly.
Errors Beyond Backend Calls
It’s widely known that almost any resource consumption operation can succeed or fail. Let’s take a look at a simple example:
DateTime parseDateFromString(String value) => DateTime.parse(value);
For those unfamiliar with Dart, the above method is used to convert a string into a date. It seems straightforward, but something you might not know is that the DateTime.parse
method throws an exception if value
is empty or in an incorrect format.
To solve this problem, we can modify the code as follows:
DateTime? parseDateFromString(String value) => DateTime.tryParse(value);
With this approach, the error still occurs, but it’s handled within the method’s try/catch
, returning null if the parse
fails.
This example shows that even when a method seems safe, errors can still occur. Of course, unit tests can help cover these use cases, but that’s not the focus of this article.
A language that aggressively handles errors is Go
. In Go
, operations return errors by default. It’s possible to write a method that doesn’t return an error, but the language’s philosophy is to make it clear that consuming a resource can fail.
Here’s an example:
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
Notice that even on success, the nil
error response is sent. Therefore, when consuming the loadPage
method, we need to check if an error occurred.
Implementing This in Your Projects
A library that brings a similar approach to Dart as Go is fpdart. With it, we have access to several functional paradigm tools. One of the most useful is Either
, which allows multiple return types for a single call, as in the example below:
Either<FailureObj, SuccessObj> someRequest();
It’s important to note that this behavior can be implemented without additional packages. There are articles like this one that teach how to do it. However, I use fpdart
for the other tools it offers, such as Task
and Option
.
Using this pattern, we ensure that any call can return an error or success, making the handling for each case explicit in the code.
Another crucial approach is creating custom error contracts for the application:
class FailureException implements Exception {
FailureException({this.message});
final String? message;
}
With a custom error class created, we can use it throughout the application, and even extend it as needed:
Either<FailureException, SuccessObj> someRequest();
I prefer creating errors by functionality because it clarifies the context in which the error occurs.
class AuthException extends FailureException {
AuthException({
required super.message,
required this.errorCode,
});
final String errorCode;
}
class ProfileException extends FailureException {}
Why not just use FailureException
for everything? Because of the tools that monitor application crashes. Imagine if all errors had the same name: we would have to rely on the error message or stack trace to try to locate where the problem occurred.
Not every error will be typed, of course, but our AuthException
will serve for known errors. This way, we significantly reduce the occurrence of generic errors in the application.
A simple tip that can help in this error analysis process is to send the custom error to the application’s logging tool:
try {
// ...
if (response.statusCode != 200) {
final error = AuthException(
message: response.body['error'],
errorCode: response.statusCode,
);
logger?.error(error);
return (...);
}
// ...
} catch (e) {
final error = AuthException(
message: e.toString(),
errorCode: '500',
);
logger?.error(error);
}
With this approach, it becomes easy to search your logging tool for AuthException
or ProfileException
, for example, and find the cases that actually occurred in these functionalities.
Conclusion
This content may seem simple and superficial, but when problems arise — and they always do — you’ll be glad you prepared your application to handle them in the best way possible.
That’s all for today, folks! See you next time.
Top comments (0)