We want our software to be error-proof, but in reality, error scenarios will always exist. So this article's objective is to explain why you should handle errors on your app, give you a rule of thumb on when treating errors, and give some practical improvements you could apply on your app. So, why handling errors? Finding errors asap Worse than finding a bug on your production app is learning that the bug you just found is around for several releases. Below is the recipe of failure to ship a hard to catch bug. { phone = telefoneNumber { } } func callMerchant (with telefoneNumber: String?) guard let else return // Calling a phone number code goes here If for some reason this function is ever invoked with a , it will exit without trying to make the phone call, and this could lead to a useless call button on your user interface, that does nothing when pressed. nil telefoneNumber Improving User Experience (UX) It might be really frightening to a user with no technology background to be prompted with an error dialog full of tech words and error codes, especially if he just made a critical action like a purchase. Take a look at the picture on the left. On the other hand, the picture to the right explains what happened and how the user could proceed. Recovering from an error state to a success With a really well-crafted error recovering strategy you could even recover a user that got in an error state, to the main flow of your application that will lead to a goal (like making a purchase). Recovering from errors is not only important for the tech team, but it is also beneficial for the business as a whole. Commonly, digital products lose some conversion percentual points due to techinical issues. And good error handling might mitigate this issue. The example above gives clear instructions and even some shortcuts on how to get out of this error and try another product. Rule of thumb to handling errors Maybe your App is nothing like any of the examples I gave so far, and you are not sure where you could improve error handling in your app. So you could follow this rule of thumb to know where you should consider handling errors. Consider handling errors every time you... ...make a request to an external source (networking) ...capture user input ...encode or decode some data ...escape a function prior to its full execution (early return) Practical improvements for your App Monitoring tool This is the most important improvement that you could do! With a monitoring tool, you can have useful data to discover, understand, and prioritize errors. By understanding the volumetry of an error, you could decide between adding a fix to the next version, creating a new version as soon as possible just to fix that error (hotfix), or turn off some remote configuration to disable the problematic feature. There are several monitoring tools available on the market, like or . The monitoring tool that I use at iFood is . It provides all the utility that we need to keep track of error logs: Dynatrace New Relic Logz.io Logs over time Querying for specific logs Configuring alerts to send to Slack Dashboard creation With a good tool setup in the project, it is time to bring a monitoring culture to the team. You could start establishing some new tasks that should be done at every new feature development. Map all error cases Create logs for the error cases Create alerts for the logs to get any critical scenario. It is important that the alerts are sent to a channel where all the devs have access. Create a Dashboard containing all the logs for that feature Monitor the dashboard periodically. You could make a recurrent event on the calendar to be reminded. Swift's Error protocol Swift's error protocol allows you to create expressive errors that will give you useful information to find and act on a possible issue. Having rich errors will also help you to create insightful dashboards and precise alerts on your monitoring tool. Below, there is a simple example of how to use it in an enumerator. { generic network(payload: [ : ]) } : enum SimpleError Error case case String Any In this example, we will capture the payload of the request that caused a network error. Doing so, we could look for patterns on the payloads, and understand what really causes the error. : if you want to do an error treatment like that one where you send the payload from a network request, you must mask any user sensitive data. Disclaimer You could also conform to this protocol in a so you can have as much information as you need on the error. This is useful when you want to custom tailor an error for a very specific scenario. struct { { one two } line: file: type: isUserLoggedIn: } { (line: , file: , type: .one, isUserLoggedIn: ) } : struct StructError Error enum ErrorType case case let Int let String let ErrorType let Bool // ... func functionThatThrowsError throws throw StructError 53 "main.swift" false Later on your monitoring tool, you could create a dashboard comparing the volumetry when the user has logged in again when it is not logged in, so you can understand if this is a relevant factor that leads to the error. And if you want the error to also contain localized human-readable messages to display, you could also conform to as shown below. LocalizedError { emptyName invalidEmail invalidPassword } { errorDescription: ? { { .emptyName: .invalidEmail: .invalidPassword: } } } : enum RegisterUserError Error case case case : extension RegisterUserError LocalizedError // errorDescription is the one that you get when using error.localizedDescription var String switch self case return "Name can't be empty" case return "Invalid email format" case return "The password must be at least 8 characters long" Now if an error of type happens, you could display to the user. RegisterUserError error.localizedDescription Don't use as an error nil Take a look at the function below, it is uncanny how familiar this code is. -> ? { dataFromKey = .standard.data.(forKey: ) data = dataFromKey { } decoder = () userPreferences = ? decoder.decode( . , from: data) userPreferences } func getUserPreferences () UserPreferences let UserDefaults "user_preferences" guard let else return nil let JSONDecoder let try UserPreferences self return At first glance, pretty standard implementation. Nothing wrong with that. But if this code ever returns , how would you know if there are no set yet or if there is something wrong with our encoding or decoding of this object? nil UserPreferences If we want to ship this code to production it would be really nice that the caller of this function would create and save a new with default preferences if there are none set, or log an error to our monitoring tool if the decoding failed, so we could investigate and fix it. UserPreferences Returning when some error occurs really limits the options you have to handle it. nil You could instead make the function throwable, declaring it with and removing the optional mark from the decoder's . If you never used in Swift, I strongly recommend you read this , as I'll not cover the basics on what it is and how it works. throws ? try throws article from Sundell -> { dataFromKey = .standard.data.(forKey: ) data = dataFromKey { .noUserPreferences } decoder = () userPreferences = decoder.decode( . , from: data) userPreferences } func getUserPreferences () throws UserPreferences let UserDefaults "user_preferences" guard let else throw UserPreferencesError let JSONDecoder let try UserPreferences self return on the example above, it would be correct to return on the clause of the , as is in fact the representation of the absence of value. I went all-in on throws just to illustrate better how it could be used. Note: nil else guard nil But what if the errors happen in an asynchronous context? Is it time to return ? Still, not ideal, instead, we could use other Swift's nice feature for error handling that suits really well in asynchronous contexts: . nil Result is an with the form that has a case with the result of the request as an associated value, and a case that brings an associated. Again, if you never heard of before, I strongly recommend this quick read from . Result enum Result<Success, Failure> where Failure: Error success failure Error Result hacking with swift If our fetch its data from a server instead of , we could rewrite it like the example below. getUserPreferences UserDefaults -> ) { .request(.userPreferences(userID: id)) { result result { .success( data): { decoder = () userPreferences = decoder.decode( . , from: data) completion(.sucess(userPreferences)) } { completion(.failure(error)) } .failure( error): completion(.failure(error)) } } } func getUserPreferences (userID id: String, completion: @escaping (Result<UserPreferences, Error>) Void Network in switch case let do let JSONDecoder let try UserPreferences self catch case let This way, the caller function could differentiate an encoding error from a network error, and log it. Separate error handling from the actual functionality If you are familiar with the principles, you know the importance of the . The says that our software units should have a single responsibility. What is a responsibility depends on the size of the software unit, the responsibility that a function can have is more narrow than the responsibility that a class or a module could handle. SOLID Single Responsibility Principle (SRP) SRP Take a look on the example below: { user.name.isEmpty == { .emptyName } isValid(email: user.email) { .invalidEmail } isValid(password: user.password) { .invalidPassword } } func registerUser ( user: User) _ throws guard false else throw RegisterUserError guard else throw RegisterUserError guard else throw RegisterUserError /* Code that registers a user goes here */ The function breaks and it is not easy to read because the code that actually register the user is at the end of the function, with all the validation rules first. registerUser SRP We could greatly improve this function by separating the error handling responsibility from the user registering responsibility. { validateUser(user) } { user.name.isEmpty == { .emptyName } isValid(email: user.email) { .invalidEmail } isValid(password: user.password) { .invalidPassword } } func registerUser ( user: User) _ throws try /* Code that registers a user goes here */ func validateUser ( user: User) _ throws guard false else throw RegisterUserError guard else throw RegisterUserError guard else throw RegisterUserError Conforming to the made this function that much easier to read. SRP Recap 😌 Don't leave errors unhandled, your users will appreciate it. ☁️ Use a monitoring tool on your project. ❤️ Use Swift's protocol to get expressive and useful errors. Error 🙅♂️ Don't use as an error. nil 👨💻 Separate error validation and treatment from the actual functionality. What's next? If you are interested in error handling, chapter 6 of is a must-read. This chapter was the biggest reference to this article, and it was what really drew my attention to the importance of handling errors. I consider this book a must-have for any software engineer, so you might as well . Clean Code from Robert C. Martin (a.k.a Uncle Bob) get it on Amazon If you want to know more about and the other principles, is a great light start. And gives nice and easy examples and has links to in-depth articles that are really good reading. SRP SOLID this article at hackernoon This repo from ochococo Thank you for reading! I hope this is was insightful and that you can apply those ideas on your projects. Take care and good error handling! Previously published at https://lucasoliveira.tech/posts/improving-error-handling-in-your-app