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
nil
func 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