Top-down iOS error architecture
How to handle errors in iOS apps
There isn’t a lot of articles about error handling in the iOS ecosystem, even this seems to be a non easy and substantial component of each app. Today, I wish to tackle this problem and present my solution towards less disruptive error handling in the iOS app.
To ensure that we are on the same page, by an error I will mean an instance that conforms to Swift’s
Error type and error handling is a reaction to it. These errors may arrive in a synchronous manner (disk operations, invalid argument) or asynchronously (no Internet connection, session token invalidated etc.). First group is obviously easier to deal, thanks to Swift’s standard
catch structure. Very often, straightforward bottom-up procedure is sufficient, where an error breaks execution and it is automatically passed to a caller. If you have an asynchronous API with completion handler, developers are free to choose among plenty of techniques (to mention promises or RX streams) that also frequently follows the bottom-up model. In general bottom-up means that we propagate an error to our parent layer if we cannot handle it completely.
I found out that in many cases bottom-up direction is not a best approach and leads to a frustration when an error has to traverses plenty of layers up just to reach the one that is able to consume it. To give you an example, let’s assume that
401 Unauthorised HTTP response, and your app should return to the initial login screen. Does it sound familiar for you to check if completion handler in a ViewModel is able to handle particular error, if not pass it to its Flow Coordinator, which then passes it to the parent(s) Flow Coordinator to finally reach to a generic place where you instruct the navigation controller to move back to the root. It ends up with a code that switches on an error extensively, like:
In case you chose delegate mechanism for a communication between distinguish elements, your delegate protocols may start to swell — every coordinator should include error events of its nested children. I observed that in many apps upper layers in a tree can handler only some specific errors (like session invalidated or unsupported API version) and below there is an aggregation point where all errors are presented to the UI (in a form of alert view or similar). So instead of starting from a bottom layer, I suggest reverting the propagation order, and start from the top layer and pass it down only when the parent layer wasn’t able to handle it.
Note that in a root element we obtained a single aggregation point of all errors in the app what may be useful for analytics purpose.
So, let’s will divide error handling process into two phases:
- Phase 1: propagate events up to its parent (unconditionally)
- Phase 2: propagate event down, layer by layer to find one that can handle it
Implementation of a top-bottom error handling architecture is not a difficult task. Let me demonstrate you that you can easily implement it within 50 lines of code with a genetic protocol oriented solution that is open for extensions.
For the purpose of this article, I will use function names inspired by Swift’s keywords list (
finally). While some of you may take it as a bit controversial, I believe such analogy helps understanding it.
We will define the only one protocol,
ErrorHandleable, that will used as a node in an error-handling tree. Each instance keeps a reference to a parent layer used in the Phase 1 propagation and encapsulates a generic closure (
HandleAction) used in the Phase 2, that decides whether it should be populated down or not (throws an error or not).
API is very concise and consists of only two functions:
throwis a function that begins handling an error (Phase 1) and receives an optional closure
finallythat is called after the handling process with an argument informing whether some layer did eventually handle it or not
catchreceives an error (Phase 2) and decides if it should be populated down or not
Take a look how could potential API call may look like:
Note that when using this API, contrary to protocol definition, you don’t have to escape keyword-reserved names (like
throw). Swift infers that we mean function instead of expression here.
First, we built up a handling tree, where
previousErrorHandler is some already existing handler received by some dependency inversion technique (e.g. dependency injection), and define two catch functions that potentially may handle an error. In a case that we observe an error in an asynchronous manner we
throw it into our
errorHandler instance to handle it. According to top-bottom procedure, it first climbs up into a root element, and then we visit all the nodes in the reversed order to finally find an element that completely consumed it (non-thrown action errorHandler#2).
In addition, nothing refrains from rewriting an error in an action block. Just throw completely new error (let’s say in errorHandler#1) and all beneath layers will observe modified one. However, remember that with great power comes great responsibility and use this capability with caution…
This protocol API is very generic, so we can add some extensions for convenience usage:
- constraint caught errors into a generic type and transparently pass all others:
2. constraint caught errors into a single value (for a type conforming to
3. listen only for a particular type (or
Equatable value) and never handle any error:
Sky is the limit for more protocol oriented extensions. For instance, you may add a function useful for synchronous API, that resembles
do keyword — takes a throwing closure and begins a top-down handling in case of thrown error.
Presented above protocol definition is rather a starting point that can be easily customised for you specific needs —for instance I can imagine a use-case where you wish to control also Phase 1 propagation…
One possible implementation
Now, let’s try to create one possible implementation of
ErrorHandler. Root handler obviously contains no parent so its type has to be
Optional and its default action is totally-transparent one that always propagates it down (functions are first-class citizens so nothing refrains us from introducing a default parameter in a signature):
Then, let’s provide an implementation of a required function
throw, that accumulates an array with a path nodes:
When throw reaches the root node it performs Phase 2 by calling
serve function which is also very simple: it calls an action and depending if it throws an error, passes it to a next element in a responsibility chain (
next) or breaks it and calls
The only one missing step is implementation for a
catch function: it creates a child element with a defined action and current node defined as a parent:
If you combine entire
ErrorHandler implementation, it takes only 50 lines of code. Code implementation with all extensions is available here for reference.
Having implementation of
ErrorHandler ready, we can take a look how would it look like in a real application, let’s say with MVVM+FC (Model-View-ViewModel + FlowCoordinator) architecture.
In a root ViewModel we instantiate
ErrorHandler. Thanks to a fact that all errors within our app have to visit this node we have a perfect chance to automatically send it to our logging framework.
Once we have attached all the listeners, we pass
rootError all the way down the view hierarchy, here
RootCoordinator which does not handle nor listen for an errors.
Let’s assume it's child coordinator
LoggedInCoordinator can handle one specific error
ApplicationError.sessionInvalidated and presents any other
ApplicationError with an alert. To achieve it, in
LoggedInCoordinator we have to keep a strong reference to a parent handler and every time our coordinator sets up a new child, it builds a new handler using computed variable
errorHandler. I used computed variable since we cannot assign it once in the initialiser as it’s action references to
self , not fully initialised at the initialisation yet. When defining closure-based action remember to always keep a weak reference to
self, otherwise you may easily introduce retain cycle and memory leak.
Once we obtained an error asynchronously while the app is running in
ScreenViewModel, we start handling it by calling
As a result we achieved a simple API where:
- entire error handling code is isolated in some parent element (here
LoggedInCoordinator) and rest of a code does not care about it (unless it wishes to),
- action handlers are type-safe,
- code models native Swift syntax what improves its readability,
- all hierarchy elements depend on a protocol
ErrorHandleable, so isolating it for the sake of unit testing is not a problem.
Standard approach for handling an error by propagating it up does not scale well and if your code contains a lot of layers (nested coordinators or controllers), communication between them may easily get our of control. With a presented above top-down approach, all the errors traverse in the reverse order — from a root to a child layer — so parent layer decides if it is able to handle given error and its children do not necessarily have to care about it.
Implementation of such top-down architecture is quick and straightforward so in my opinion there is no need to introduce any third-party library, even any μFramework. You benefit from a solution that perfectly meets your requirements and you get rid of one external dependency. Perfect win-win!
If you have any comments or any other error handling patterns to share, don’t hesitate to let me know on twitter @norapsi.
If you are wondering, what it has to do with a pinball game, here is an animation:
Thanks to Martyn Pękala and Jenus Dong for valuable suggestions.