The Importance of Error Handling in Mobile Applications

gguedes

Gustavo Guedes

Posted on August 21, 2024

The Importance of Error Handling in Mobile Applications

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

With a custom error class created, we can use it throughout the application, and even extend it as needed:

Either<FailureException, SuccessObj> someRequest();
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
gguedes
Gustavo Guedes

Posted on August 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related