Error trees in iOS

Isaac Weisberg
12 min readOct 24, 2019

--

Thanks to undraw.co for a great illustration

In this article, I would like to discuss the concept of an adventurous and thoughtful error handling in a modern iOS application.

The concept consists of two major aspects:

  1. Building error trees
  2. Producing the user-friendly error state visualization lazily

All the code presented in this article can be found and fiddled around with here.

Error tree is a compound data structure that is designed to make it easy to see what exactly is the reason behind a failure of a complex operation.

— literally me, writing this very article.

The main premise here is to treat your error objects as regular models, identical to the objects that your view would use to configure itself during positive, error less use case. This emphasis dictates that the creation of an erroneous model to be used by a view may or may not be just as complex as a creation of any other model. In a typical heavyweight architecture you would see that the creation of a model that view sees is preceded by some intermediate steps such as creation of an abstraction over the network’s response, a so-called “data transfer object”, that encapsulates the deserialization logic while not carrying anything related to the business rules of the application.

Alternatively, you could see that a business logic unit (again, view model or a presenter, or a stateless interactor) would wrap the model it received from the model layer of the application into its own abstraction. That possibly introduces several use-case specific transformations to the initial object. Heck, even view might have an interest in building an abstraction above the stuff that it receives from a business logic unit. Well, the concept of an Error Tree is more of the same.

Let’s imagine an architecture in which there is a business logic unit (a view model) and a stateless functionality layer (a model or a service). Your goal as a programmer is to

  1. Accept a UI event in a view model
  2. Perform an operation on a service
  3. Perform a business logic-specific validation of whatever the service produced
  4. Mutate the view model’s state and propagate it to UI

For the reference, I have written a full-blown project which follows those business rules. Meet the weather forecast application.

The flow is as follows:

  1. The user taps the request button on the MainViewController, the event is forwarded to the MainViewModel
  2. The view model loads the forecast using the ForecastDownloadService
  3. The view model validates the temperature in the forecast so that it’s in a hard coded range (just for the sake of experiment)
  4. If any errors occurred — propagate, additionally propagate the last known actual temperature.

I.e. simple as a cake. As a result of all of these manipulations your UI may or may not end up looking like this:

Here’s a catch: there are a lot of things that can go wrong during this retrieval: (starting from view going up to model)

  1. The temperature was not in a valid range. The logic is implemented in the MainViewModel.
  2. The forecast was malformed and the parsing process failed. The logic is implemented in the ForecastDownloadService.
  3. The networking layer relied on the Apple Foundation’s networking capabilities, URLSession in particular and there was an inconsistency where a data task handler was called with the wrong subclass of URLResponse which was other than the expected HTTPURLResponse (impossible IRL, but you have to cast manually nonetheless). This logic is implemented in DataDownloadService.
  4. The HTTPURLResponse status code was not 2xx. Also in DataDownloadService.
  5. Data task handler didn’t receive any data, hence nothing to parse. Also in DataDownloadService.
  6. Data task called the handle with an error object, hence TCP failed. This is also in the DataDownloadService.

In a case, if we aren’t too picky about how we handle our errors, we might be satisfied in an exceptioning-esque mechanism that upon an error at any point just unwinds the stack, propagating the inner error to the outermost catcher. We, however, deal here with programming for real goofsters and gafsters, after all the networking using URLSession is primarily asynchronous, so we use Reactive Extensions. With them we also can take the approach of “errors at any point — just return to the outermost caller who’s catching”.

But here’s the real catch, boi. Come, your stuff depends on several network requests, each one requiring a parsing procedure, and suddenly one of them fails and you get this transparent, generic Error object produced by a JSONDecoder.decode(_:from:) call. How can you possibly know which one of the processes has tilted?

And then, to make matters even worse, what could you possibly do in a case where business rules dictate that in case of failure of one process your view should display error in one fashion, while for the other process’s failure — in another?

Furthermore, golden case in any real-life commercial development — the HTTP status code maybe 401 so you need to perform logic that isn’t even important to the view we are currently building but is rather the application’s concern, yet you need to seamlessly manage this case by organically showing a login/sign up view controller, without actually removing the view where the error originated.

Here’s my take on this problem — a tree of errors.

The concept is quite similar to the stack trace except designed not for debugging/manual navigation/code editing but properly embedding the thing into the actual business logic, embracing the static typing and carrying information over to anyone below who might need it.

Getting back to the DataDownloadService, let’s introduce a model that features all the cases of errors that might occur during the service’s operation.

Note that the errors are individual to the call, the operation of a service. So for each method there might be its own family of errors.

Then, we will have a ForecastDownloadService which depends on DataDownloadService.

And what you can immediately see is that if the operation that this operation depends upon results in a DataDownloadServiceError, we don’t just propagate it up top to our caller but instead wrap it into our own abstraction so that our user can deduct whose exactly fault it was that we failed.

Let’s cover the business logic inside the MainViewModel.

Here there is no reason to keep the erroneous enum visible outside of the scope of view model since the view will be receiving the state in a “digested” manner. You’ll see what I mean in a bit.

Oooh, we dun prepped up, now I’m going to showcase just how we utilize it.

The imaginary product manager says that if nothing critical has happened, we are free to just show a message at the bottom of the screen. We obviously don’t show that message if there is no error. And we won’t show the message if the issue is major because we will certainly have other means to show it to the user.

Our MainViewModel now has this bit here:

Lots of code, but in a nutshell, the minorError is fed with a user-friendly string only in case of networking error coming from URLSession because the other cases indicate that either the server is drunk or we’ve programmed something badly. Or maybe the status code was 401, we’ll handle it separately.

Here comes the usage:

Alright, we’ll be temporarily moving on from the topic of error trees itself and touch another aspect that comes in handy. We will proceed with implementing an error display via an alert controller later.

Here are a few good questions to ask while I’m finishing this paragraph. What if the view doesn’t want the string message that we feed it? What if the message that we feed into the view is not just a message but a message and a title, or a couple of messages? What if we make a mechanism in which there should be an alert dialogue and the result of it is fed back into the view model, and for each possible event the button names, title, and description differ? What if the view is way more complex feeding tons of resources into configuration including localizable strings that are reactively consumed, and also images, maybe colors for the dark mode and a very huge time and resources consuming setup to create it all?

I’m gonna show you another pattern we used to adopt back at the factory.

Avoiding representation of your errors with raw types

For this message at the bottom of the screen, we need a single piece of information — and that is the text. Let’s make ourselves an abstraction on top of it that allows us to determine the user-friendly messages lazily.

protocol ErrorSingularType {    var singluarDescription: String { get }}

The name says that the information carried in this unit is singular i.e. just one description. The description that I propose we hold there should be already user-friendly, localized text. What we then do is have a, dare I say, factory type for it:

protocol ErrorSingularRepresentable {    var errorSingular: ErrorSingularType { get }}

The rule of thumb is if an object conforms to this protocol — it can be quickly converted into a user-friendly message containing an object by a mere call of a synchronous getter. Now let’s see how we use it when someone gives this to us:

let errorText = error.errorSingular.singularDescription

where initial error conforms to that protocol.

We are abstracting the process of creating a user-friendly description with the primary goal of separating this process from propagation and business logic handling mechanisms. And that’s practically it.

After this bit we will be concerned with implementing an alert controller to be shown on certain errors, so let’s have a user-friendly model that is capable of configuring an alert controller. Won’t bother with the buttons, just the title and description.

protocol ErrorTitledSingularType {    var title: String { get }    var singularDescription: String { get }}

The description getter has the name compatible with the ErrorSingularType just for these cases where maybe your view will decide to toss away the title, treat it like a singular error.

Before we proceed, to reiterate: only the view should decide to produce user-friendly representations of the errors, and no one else.

Alright, an alert then. An alert controller should be shown for major errors. In our case, it’s almost all of them by the common rule “if it’s the servers/clients programmers fault, then it’s a major one”. Essentially, every single error but the URLSession’s networking one. Also, we will later not treat status 401 as a major error, but that’s a bit later.

First, let’s give our error tree the ability to produce user-friendly titles and descriptions for the new alert:

We do provide localizations for each and every single situation just in case — maybe someone else will be interested in using these.

Here, we don’t localize the errors we depend upon on our own, instead we ask them to localize themselves.

Same deal with the MainViewModel and its errors.

Can’t be bothered to post the error selection to gist. You can imagine: an error arrives, and then I switch through the whole tree, ignoring only the innermost DataDownloadServiceError.networkError and propagating the whole tree to the view, this is the most important aspect.

And here is the usage:

Please note the call to the getter errorTitleSingular. This is the place where the error object converts into something unusable for implementing custom business logic but is a perfect fit for being used in an alert.

This whole thing yield results like this. Note that the bottom message is not there.

Alright, done with that. While at it, I am going to change the architecture of the DataDownloadServiceError so that unexpectedStatusCode is separated from the exclusive 401 status code case. The new case will be called unauthorized. Updating the logic we’ve written so far, this error is nor minor, nor major. It’s critical and will require us to show a new view controller.

This auth error controller will be configured using three separate units of user-friendly stuff: a title, a description, and an image. Our primary focus with this example, however, is to see that this auth error controller will rely not on well-defined structure, but rather on a protocol, the implementers of which can do whatever they want.

For instance, we expect this AuthErrorController to be reused throughout the app and support in its rendering the context in which this controller was instantiated. Here, when we are coming from the MainViewController the AuthErrorController might want to say something like “you were trying to get a forecast, but your auth is busted”. While for instance if we had someone's profile with their data about their favorite types of weather and even location, an AuthErrorController might say something like “You need to be logged in to view this user’s profile”. We will allow the stack that produced the auth error to decide how to visualize it in the AuthErrorController.

Let’s define the common interface.

protocol AuthErrorModelType {    var title: String { get }    var description: String { get }    var image: UIImage { get }}protocol AuthErrorModelRepresentable {    var authErrorModel: AuthErrorModelType { get }}

Then, we provide an implementation that is oddly specific for the flow of the MainViewController and is not meant to be used by anyone else.

Then in the MainViewModel we filter out all the wrong errors and create the lazy AuthErrorModelRepresentable.

And then — the navigation. I didn’t bother with coordinators this time around, just writing in the app delegate.

And hey, presto! You twist the business logic around errors all you want, never lose a single bit of information, and also you are creating stuff lazily. Albeit cumbersome, these practices have proven to be worth it in the long run in both 2 companies where we have tried it. There is no necessity for it in trivial cases, of course, it would only raise maintenance costs. But if you ever come to think about refactoring your networking layer, this might be a cool little idea to grind through.

And that’s it. Please check out the source code of the resulting application and I would be rather thankful if a conversation could spark.

About Me

Hi, I’m Isaac, I’ve been doing a bit of full-time commercial iOS programming for nearly 3 years and I’m just like everyone trying my best not to procrastinate.

Hey, if you liked this article 🤔
1. please clap once 🙏
2.
follow me on GitHub 🥴
3.
check out my website 👬
4. my wife left me

Post Scriptum

This article is inspired by this Medium Flawless iOS article, authored by Anton Nazarov, please do check out his profile and his articles. There, in the “Typed Errors” section he’s wondering:

Honestly, I do not know why do we need an explicit error type. For most cases, we work with errors as something abstract. We have some generic pop-up where our favorite “Something went wrong…” is placed. Otherwise, we do not want to handle errors and prefer to show the usual empty screen or error state. I am a big fan of error handling as a separate stream and very rarely work with a specific type. In our project, there is no custom type that conforms to Swift.Error.

And this is one of the brilliant cases where typed errors in the RxSwift would be of great use since the regular mechanism causes all error objects to just lose it’s static typing. In places where we were attempting to kick off this architecture and the code would rely on RxSwift’s error propagation mechanism we were like “We DO need to know what exactly went wrong” — and then something like this would’ve been written:

.catchError { error in
assert(error is LEOServiceError, "Inconsistency detected")
let error = error as! LEOServiceError
// ...
}

As a result, all the new code just supposed that the onError event is never used and we just propagated Swift.Result.

That is if you don’t just materialize your observables and treat errors as generic.

--

--

Isaac Weisberg

iOS. Doing something with my life. Good food and good time.