Finding errors asap
func callMerchant(with telefoneNumber: String?) {
guard let phone = telefoneNumber else { return }
// Calling a phone number code goes here
}nil telefoneNumber, 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.Improving User Experience (UX)
Recovering from an error state to a success
Rule of thumb to handling errors
- ...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
- Logs over time
- Querying for specific logs
- Configuring alerts to send to Slack
- Dashboard creation
- 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
enum SimpleError: Error {
case generic
case network(payload: [String: Any])
}struct 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 StructError: Error {
enum ErrorType {
case one
case two
}
let line: Int
let file: String
let type: ErrorType
let isUserLoggedIn: Bool
}
// ...
func functionThatThrowsError throws {
throw StructError(line: 53, file: "main.swift", type: .one, isUserLoggedIn: false)
}LocalizedError as shown below.enum RegisterUserError: Error {
case emptyName
case invalidEmail
case invalidPassword
}
extension RegisterUserError: LocalizedError {
// errorDescription is the one that you get when using error.localizedDescription
var errorDescription: String? {
switch self {
case .emptyName:
return "Name can't be empty"
case .invalidEmail:
return "Invalid email format"
case .invalidPassword:
return "The password must be at least 8 characters long"
}
}
}RegisterUserError happens, you could display error.localizedDescription to the user.Don't use nil
as an error
nilfunc getUserPreferences() -> UserPreferences? {
let dataFromKey = UserDefaults.standard.data.(forKey: "user_preferences")
guard let data = dataFromKey else { return nil }
let decoder = JSONDecoder()
let userPreferences = try? decoder.decode(UserPreferences.self, from: data)
return userPreferences
}nil, how would you know if there are no UserPreferences set yet or if there is something wrong with our encoding or decoding of this object?UserPreferences 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.nil when some error occurs really limits the options you have to handle it.throws and removing the optional mark ? from the decoder's try. If you never used throws in Swift, I strongly recommend you read this article from Sundell, as I'll not cover the basics on what it is and how it works.func getUserPreferences() throws -> UserPreferences {
let dataFromKey = UserDefaults.standard.data.(forKey: "user_preferences")
guard let data = dataFromKey else {
throw UserPreferencesError.noUserPreferences
}
let decoder = JSONDecoder()
let userPreferences = try decoder.decode(UserPreferences.self, from: data)
return userPreferences
}nil on the else clause of the guard, as nil 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.nil? Still, not ideal, instead, we could use other Swift's nice feature for error handling that suits really well in asynchronous contexts: Result.Result is an enum with the form Result<Success, Failure> where Failure: Error that has a success case with the result of the request as an associated value, and a failure case that brings an Error associated. Again, if you never heard of Result before, I strongly recommend this quick read from hacking with swift.getUserPreferences fetch its data from a server instead of UserDefaults, we could rewrite it like the example below.func getUserPreferences(userID id: String, completion: @escaping (Result<UserPreferences, Error>) -> Void) {
Network.request(.userPreferences(userID: id)) { result in
switch result {
case .success(let data):
do {
let decoder = JSONDecoder()
let userPreferences = try decoder.decode(UserPreferences.self, from: data)
completion(.sucess(userPreferences))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}Separate error handling from the actual functionality
func registerUser(_ user: User) throws {
guard user.name.isEmpty == false else {
throw RegisterUserError.emptyName
}
guard isValid(email: user.email) else {
throw RegisterUserError.invalidEmail
}
guard isValid(password: user.password) else {
throw RegisterUserError.invalidPassword
}
/*
Code that registers a user goes here
*/
}registerUser breaks SRP 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.func registerUser(_ user: User) throws {
try validateUser(user)
/*
Code that registers a user goes here
*/
}
func validateUser(_ user: User) throws {
guard user.name.isEmpty == false else {
throw RegisterUserError.emptyName
}
guard isValid(email: user.email) else {
throw RegisterUserError.invalidEmail
}
guard isValid(password: user.password) else {
throw RegisterUserError.invalidPassword
}
}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?
Previously published at https://lucasoliveira.tech/posts/improving-error-handling-in-your-app