Digital Advisory Customer Experience Technology
Izabela Franke
Digital Advisory CX Strategy Retail
Jakub Nawrocki
Digital Transformation Retail
Paweł Wasilewski
Values People
Tomek Jurek
Digital Advisory M-commerce
Izabela Franke
Digital Advisory UX research
Jakub Nawrocki

Featured Insights

Explore all insights
Close
Digital Advisory Customer Experience Technology
Izabela Franke
Digital Advisory CX Strategy Retail
Jakub Nawrocki
Digital Transformation Retail
Paweł Wasilewski
Values People
Tomek Jurek
Digital Advisory M-commerce
Izabela Franke
Digital Advisory UX research
Jakub Nawrocki
Explore all insights

Featured Insights

EngineeringTechnology Mobile Development

Error Handling in Mobile App Development

Error Handling in Mobile App Development

Often, both in analyzing business requirements and in implementation, we give a lot of attention to making the success path usable and fully functional but lack love for error handling. I get it, it’s not sexy, but you will have to deal with it at some point anyway, so why not do the right thing from the beginning? This article reviews the sacraments and sins around this topic.

Exceptions that WILL occur

Most often there is no problem with those exceptions. They are quite obvious and arise out of the application’s business logic or some access to external resources . If there’s a business logic in place, it’s possible that it could branch out into different paths, one of which will be successful, and the other will fail and result in an error.

Examples:

  • Downloading data from an external website - the device may not have access to the Internet or the server may return a 500 internal server error if it’s down.
  • Registration form - the user can enter an email that is already registered in the backend.
  • Displaying user's location -  lack of location permission.

The application must be prepared for the fact that each process it implements will fall into exception paths - and the UI must be able to handle such situations .

Usually, when implemented, these paths are already reasonably evident. However, it is also important to remember about them in the case of business analysis, both for estimations and feature breakdown in sprint planning, and so on.

Additionally, it is worth making sure that the application cannot remain in a state that we did not expect - we should always have control over the possible states the UI may be in (see more below: Good practices).

Exceptions that WON’T occur (yeah, they will)

We sometimes write code that is so unlikely to fail, that we don’t really need to care about handling the failure… right?

Examples:

  • The app triggers a simple SQL query, which will surely not crash.
  • We checked the existence of a variable two code lines above, so it simply couldn't disappear in the meantime.
  • We just created this file, so we can safely start writing to it.

I will not cast the stone here - there is such a thing as overly-defensive-programming and common sense has to be used. Indeed, there are situations where it is not worth making extra money at every step of error handling. Multiplying conditional paths and trying to deal with every exotic exception, etc., might make our code unreadable. In the worst case scenario, the app will start crashing in a particular place and we will know the real scale of our neglect.

If we reckon that this error may sometimes appear, or it actually started crashing the app (according to Crashlytics), then there is no way out: we need to deal with it. The most crucial thing in such a situation is to handle it wisely, i.e. not only to mute it with a dispassionate catch, but also to actually handle the path in which it can occur, e.g. by repeating the incorrect flow or displaying an appropriate message. Most often, it is also worth logging such a bug to Crashlytics as non-fatal in order to keep an eye on the scale of the problem. More on that can be found below.

Crash vs state of limbo

In Catholic theology , Limbo (Latin limbus , edge or boundary, referring to the edge of Hell ) is the viewpoint concerning the afterlife condition of those who die in original sin without being assigned to the Hell of the Damned .  It is not equated with Purgatory, but it is part of Hell.

Yes, if the app crashed, that's not good. But even worse would be to "handle" such a crash by muting the error and leaving the application in an undefined state. The simplest example of this is when we have to load something from the network and, for example, a 500 internal server response would end in a crash. If, instead of properly handling such an error, we simply catch the exception and print such an error message to the console, we would probably leave the user on the loading screen without any information. It’s an awful UX. A special place in hell ( limbus, pandemonium ) is reserved for developers who allow limbo from which it is impossible to escape, where the only option for the user is to restart the app (or, horror of horrors, reinstallation).

But the worst part is that if we do mute the error out of laziness, in many cases we may never notice it again. At least a crash would be reported to Crashlytics and therefore could be corrected.

The Cardinal Sins

Summing up, as a developer, if you don’t want to end up in hell, avoid these Cardinal Sins of Error Handling:

  1. try, and in catch, only printing an exception error message to logs ...
  2. ... and generally, allowing an application to be left in a state of limbo ...
  3. ... and the worst is if it is not possible to recover from this state.
  4. Neglecting to handle business logic edge cases - those crashes that will surely appear.
  5. Failure to consider paths other than the happy path in estimations.
  6. Brushing users off with a generic (“an error occurred”) message when there’s more to tell them.

Good practices

Do not be afraid of exceptions

There are programmers who do not use exceptions, probably because they don't like to make the association “exception = crash”. Proper use of exceptions can work wonders for code that handles edge cases but wants to be clean at the same time. The main rule could be summed up like this:

  • throw in a class where a certain error can occur and the class doesn't know what to do with it.
  • catch in a class that knows what to do with certain errors.

In particular, exceptions are not a substitute for the normal control flow of an application, which is something that can be handled with a regular conditional expression. However, there are times when we call a function of a given class and that function simply wants to say " I tried, but I can't ." Then normal control flow would have to end up returning, e.g. a null, which is far from being useful or informative.

A nice extension of the topic can be found here .

Catch exceptions only in the class that knows what to do with them

As a quick reminder:

  • throw in a class where a certain error can occur and the class doesn't know what to do with it.
  • catch in a class that knows what to do with certain errors.

However, there may be other classes along the way that do not meet either of these conditions. Sometimes these intermediate classes can add more context to the exception - in that case wrap such exceptions. On the other hand, a common mistake leading to latency of side effects is catching the exception too deeply: in a class that doesn't know what to do with it. Most often it ends in such a way that when a given exception pops up, it is only logged, but there is no meaningful handling of it in the UI.

Wrap Exceptions

It is very often worth using your own exceptions that provide useful information in the context of your application’s business logic. If our class does not know how to handle a given exception, but knows what exceptions may appear and what they may mean in its context, it can wrap them into something more understandable . For example, if we support encryption, system APIs can crash into a number of strange scenarios, each throwing a different exception. If our class is used to store an encrypted password, we can wrap all these strange errors into one exception PasswordDecryptionException - clients of this class will decide what to do with it, and if it ends up unhandled, at least the original stacktrace will be kept.

try { decryptString(encryptedPassword) } catch (e: UnrecoverableKeyException, KeyStoreException, IllegalStateException) { //yeah, like we have multicatch in Kotlin :P throw PasswordDecryptionException(e) }

Fail fast

Better to throw an exception and risk crashing the application than trying to continue executing the program in an undefined state. This behavior primarily allows you to create processes that are predictable, as well as detect errors early. Attempting to proceed with the code in an undefined state may cause the app to go into a state of limbo and you may never find out about it.

Closed hierarchies of ViewStates / Closed state hierarchies

To make sure that a particular view does not end up in an undefined, unforeseen state, it is worth using closed state hierarchies based on enum or sealed class. As a bonus, this approach fits perfectly with unit tests - in the test ViewModel it is easy to notice whether among our strictly defined outputs there is any edge case for a given set of inputs.

sealed class SubscriptionState {     object Unknown: SubscriptionState ()     object NotSubscribed: SubscriptionState ()     object Canceled: SubscriptionState ()     data class Subscribed (val nextBilling: LocalDateTime): SubscriptionState () }

There’s no exception in the view - there is an appropriate UI state

In  MV * class architectures, the View layer should no longer know anything about deeper layers’ exceptions. At most in the ViewModel, they should be changed to some view state. For example, if data is downloaded from the API on a given screen and it may end with an HttpException, then such information should be sent to the view as e.g. ItemsLoadingError state.

Consistent method of non fatal error logging

Due to the fact that, in every project, sooner or later you will have to log a non-fatal error to Crashlytics, it is worth having a tool that is easy to use anywhere. For example, on Android it can be a custom TimberTree:

class MyTimberTree: Tree () { override fun log (priority: Int, tag: String ?, message: String, throwable: Throwable?) { If (throwable! = null) { logToLogcat (priority, tag, Log.getStackTraceString (throwable)) reportToCrashlytics (throwable) } else { logToLogcat (priority, tag, message) } } } // and then when you need to report a non fatal Timber.e (throwable)

Don't log non fatal errors randomly

Yes, Crashlytics is free, but there is a hidden cost of logging in everything blindly - it quickly becomes a mess. As a result, it is difficult for us to notice essential errors among the mass of irrelevant non fatals. As a rule of thumb, log the non fatal in places where you have already handled the error but want to explore it further or understand the scale of the problem in order to handle it better.

Unit Tests

Last, but not least, writing testable code and the unit tests themselves makes it very easy to spot edge cases and any paths beyond the happy path.

Working with Crashlytics

Look for new bugs or peaks after each release

When working on a project for a long time, it is important to periodically review Crashlytics, especially directly after releases.

Use Issue Notes

It is worth using the notes feature mainly to link tasks in Jira. This allows you to find on the list of failures those that have already been addressed somehow, and it is easy to navigate to them.

Close crashes

Do not be afraid of closing issues in Crashlytics - they won’t mute. If, for example, there is a bug in version 1.2, and we fixed it in the version released as 1.3, it is worth closing the crash even before this version is released. Then, it disappears from the default view of the crash list, but if it turns out that the bug still exists in 1.3, it will not only appear there again, but you will also get an "Issue Resurfaced" email.

Log user id and other helpful data

Crashlytics allows you to set various keys that enable you to better understand when a given error occurred. It is especially helpful to set the userId, because it allows you to find errors that were reported by a specific user in another system.

FirebaseCrashlytics.getInstance (). setUserId (registrationId) FirebaseCrashlytics.getInstance (). setCustomKey ("appMode", appMode.name)

Related insights

Arrange consultation with our Digital Advisory & Delivery Team

describe your challenge
Get in touch with our experts to learn more about the benefits of having us by your side.
We engineer
digital business

Subscribe to our insights

A better experience for your customers with Future Mind.

This field is required. Please fill it in, so we can stay in touch
This field is required.
© 2024 Future mind
all rights reserved