Design an Error Handling System before you structure your App or Software Project (Part I)
Thinking about an Error Handling System in your app can improve the reliability and maintainability of your app exponentially.
Tojo Batsuuri - Lead Developer + Designer
May 8, 2019
Thinking about an Error Handling System in your app can improve the reliability and maintainability of your app exponentially. Out of the infinite possible things that can go wrong in your app, there’s only a limited number of scenarios you actually want to see happen. So when a pesky bug appears or feature changes are introduced in your app you will be glad you have an amazing Error Handling System that’s gonna tell you exactly where and what went wrong. If you have time, it is also a great idea to suggest fixes for them, or give other hints as well, in a similar way that web APIs do.
I will explain every concept in the context of making an iOS app for an enterprise solution. Specific code gists are included throughout.
The business perspective
On top of debugging and having a reliable software. Businesses often want to collect data on usability and analyze it to improve their services and offerings. So reporting errors stats in the analytics reports would be a strong indicator for how their product is serving people.
Users don’t want an app to just crash or not respond. Users want to know what went wrong so that for most errors they can try troubleshooting themselves so that they can get their current task accomplished.
The developers will save time and stress in building and maintaining the system. Developers forget their code, team of developers will work on each other’s code; so an error handling system with good names, messages and hints will make like easier for anyone who uses your code.
Like a cake, all the layers in your app give it structure. Unlike a cake, the layers in your app are just abstract.
Here is where the business logic resides. What relationships are allowed and what makes sense in the context of the business problem you are trying to solve. So things like how many coupons each user is allowed to obtain will be modeled here. The data and the models that are relevant to the business are here.
Your app will connect to external services like third party APIs and the app’s own backend. As well as the local database; for caching and storing things is regarded as an external service. So your networking and caching classes are in this layer. The parsing and supplementary models like Request and Response models are also here.
User interface layer
This layer will have things like your ViewController, View and Cell classes. This is the icing on the cake. Make it look nice.
Since making this diagram. I realized handling errors right then and there is better most of the time in practice.
You might get errors like “This operation is not allowed”, “Didn’t meet required criteria”. From a programming perspective these could be custom rules that we think of as Business Logic. If there are different types of users and authorization types, that will be decided here.
These errors often can be the same for the Developer and the User.
Here you might get errors like “Wasn’t able reach server”, “File didn’t exist”. From a programming perspective, these will often be an error propagated from the API or the database.
If the error is tech mumbo jumbo, it’s a good idea to have a seperate message for Developer and User. Furthermore give as much info to the developer as possible.
User interface layer
Here you might get errors like “Wasn’t able to resize window”, “View didn't load”. These will just be UI errors, so unless crucial for the user to know. It’s not necessary to display it to the user. However it is crucial for the developer to know these, so that they can recover from them.
Some Basic Mechanisms: Fundamentals
As the developer or project manager. You should make sure you do this part correctly to avoid spending a lot of time on things that could be avoided. Things that could be avoided include:
Every project is different. There are many tools out there. But the thing to do is, if there’s anything you do very often. Often taking time away from the problem and improving your skill and knowledge about that feature will speed up and fix many of the inefficient things you are bound to do.
Spending too much time on one aspect of bug fixing
Finding the bug. Reproducing the bug. Understanding the bug. Considering the fixes. Considering all the other parts it might mess up. Testing the bug itself. Testing surrounding features still work. If any of these take way too long, you have yourself an inefficiency.
Printing, logging, warning
The small things like printing errors and throwing exceptions are logged to console. Sometimes you wanna show it to the screen. I usually make a custom print function that may do several things: check if in development mode, vibrate, log time and message, show on screen, log to analytics and have breakpoints.
When an independent tester tests and comes across a bug. They will have to explain where and what happened after what steps. This is inefficient. You need a system for this to happen efficiently. Maybe when you show an error to QA tester person, show the error number or code, so they can tell you the code and you can find it instantly.
Another things you might wanna consider is, often you will have a path in your logic, where you just don’t want to happen or handle. Here you might wanna include a fatalError(), but that just crashes the app. This is not good, often the QA will say app crashed, making you look bad. So if you wrap your crash with a function that displays “App is about to crash!” then crash it. Then at least, it looks like you intended it to crash.
Optionals are a great way to handle errors. Especially if a function returns an optional type. Your models should have failable initializers. Use optionals and make sure to always log when it’s nil and maybe even why it might be nil.
Learn your throwing and do-catch, try, try? and try! well. Learn rethrowing and closures well.
Here make sure you don’t show developer messages or log unnecessary messages. You should show helpful messages to the user so that they can mostly troubleshoot their own problem and at worst know why it failed and give the option to report the problem. But logging minor, non fatal errors to analytics is very helpful and consider your error stats to decide which ones to fix first. Or maybe even how you can fix one thing and get rid of multiple bugs. Crashlytics, do it.
Error Sources and Classifications
It’s pretty important to get your terminology not mixed up. Don’t mix up your state and error, the state can be source of your error, but it’s not an error type. So a good practice is to map your states to custom error types, so that when you come across a logic that dictates something should not happen under a certain state, you don’t throw the state but the error representing the state.
Now if your application is layered like Model, Infrastructure and View, or really any of MVVM, MVC, MVP, you will have to structure your errors in almost the same way as your app. So the errors will be in Model, Infrastructure and View layers. This means throwing errors, then rethrowing errors and handling errors.
The best way I found to organize all your error types, is to create a file called AllErrors and include all your errors in there. Now the specific functions that throw errors will have to go everywhere in the app, so a common pattern is needed to make it more manageable. It would also be nice to have some kind of central file that manages all the error throwing and rethrowing functions. Don’t worry you will see how all this should work in a simple manner at the end of the article. For now the thing to understand is to general reasons and motivations for doing certain things. In fact the specific implementation can vary quite a bit.
What to include in your models?
All the regular stuff. But more importantly every initializer should be failable. Let me justify this. If you follow this pattern everywhere you’ll get used to it. But what purpose does it serve? You should apply data validation on every parameter in your initializer. If Person object’s initializer has a parameter Int named age, and your business requires a mature audience, then return nil for ages 18 and under. For most purposes your backend will take care of this and about 99% of the time everything will work as expected, but having this airtight data validation everywhere will make your system better. If a part of your system already handles that, then why should you handle it, isn’t that redundant and a waste of time? Well systems, and parts change. So doing this between every point where data is exchanged between large parts, will decouple your system and make it more robust, even if the other part is completely changed. In software there are many times you’re tempted to be lazy, sometimes that can lead to novel solutions and shortcuts. But mostly it just leads to an error riddled piece of shit app.
On your business relevant models, you should have basic API functions like get, set, initialize etc. If you need to have a cache layer as well as a server backend. Then your app should always have a single source of truth. Where everything in your app gets it from the local storage and the local storage somehow interacts with the server database to sync up as reachability permits.
The errors here should be: Couldn’t write and read, not authorized, custom business rules etc. All written inside the model classes.
What to include in your Infrastructure Layer?
Often I will follow the singleton pattern with my data layers. One singleton for networking related functions, one for the caching related. The supporting classes are an important piece of this puzzle that should bubble up the errors but the singletons are the most important here. Data cache class will handle all persistence and furthermore sync with the network layer.
You could have a manager class that contains the two data singletons which will manage all the complicated rules of syncing between them. Here the application state becomes important like app is online/offline or logged in/out. But the decision on how the cache layer syncs with the database is largely decided upon by the business logic.
The errors here should be relating to app state, syncing errors, external errors propagated to the app (either from third party services, backend or file system/ SQLite database).
So depending on the developer, you could handle the errors in the UI layer or near where it happens. I personally like to keep my ViewControllers light and a lot of functionality automated so I usually choose to do them near the source. If there are request and response models, then their failable initializers and related methods that throw errors are considered part of this layer.
What to include in your User Interface Layer?
Nice automated way of doing most features is through protocols. I like the idea of conforming to different protocols that represent different features and just tag them on the view controllers. It makes it clear what each controller has for features. Furthermore, by declaring the functions in the protocol extensions you hide away some implementation, get some functions that you can just call. But its also nice to be able to handle errors at the last layer to do something one off. In that case just relay the error as one of the completion closures parameters.
Not all bugs are this cute. We wanna limit them
This is but one software project design pattern that solves the many business problems and optimizes the app for maintainability and reliability. This was a general overview of the system.
I hope you took away some useful strategies for architecting your own app and solving some common problems. Stay tuned for the next article in the series, where I can explain the structure of throwing, rethrowing, closure throw functions at length. They are an essential piece of the error handling system. Then the next articles will cover: generic error/response structure, RxSwift, asynchronous error handling…