Error Handling in Swift

Well, every developer loves errors. It is crucial to determine what went wrong, and users should not see them as developers do! We have to show clear and concise errors to users. Preferably written by a copywriter. Given the importance of this, we should give the same attention and care to how we handle errors in our Swift code as well.

Error Handling

In Short

We should separate error types with associated values in a main error enum.

Why do errors need handling?

Product experts state that a user decides whether a user likes an app or not in 5 seconds. That’s why onboarding is super important in mobile apps. Users’ interest in the app is too fragile. A cryptic error or errors (yikes) reduce the app’s reliability, resulting in high churn.

On the development side, we need a system to present these errors with clear language. Also the codebase must be clean and open to maintenance.

Talk is cheap, show me your code!

Imagine that we have a MainError enum that can be used all over the project.

enum MainError: Error {
    case general
  
    var userFriendlyDescription: String {
        "MainError.\(self)".localized
    }
}

For now, there is only one case which is general. It is localized by using userFriendlyDescription.

In case the general error is being used, the 5th line will return “MainError.general”.localized. The localized is just an extension of String to return localized string by key.

extension String {

    var localized: String {
        NSLocalizedString(self, comment: "")
    }
}

So, the result refers to the related key in Localizable.strings file.

MainError.general = "An error occurred. Please try again later.";

Let’s go deeper!

We can easily extend MainError by adding a few new cases or associated values.

Almost all of the apps have a network layer that occurs errors like a popcorn machine. Imagine that we have another error enum for the network errors.

enum NetworkError: Error {
    case userNotFound
  
    var userFriendlyDescription: String {
        "NetworkError.\(self)".localized
    }
}

It has the same structure as MainError. All we need to do is add a new key value to the localizable file.

NetworkError.userNotFound = "The user does not exist";

What is the main idea of this article?

Manage all errors in one place!

Let’s merge NetworkError into MainError by creating a new case.

enum MainError: Error {
    case general
    
    case networkError(NetworkError)
  
    var userFriendlyDescription: String {
        switch self {
        case .networkError(let error):
            return error.userFriendlyDescription
        default:
            return "MainError.general".localized
        }
    }
}

Tricks

If we would like to parse the error key something like USER_NOT_FOUND that is coming from the backend response.

enum NetworkError: String, Error {
    case userNotFound = "USER_NOT_FOUND"

    var userFriendlyDescription: String {
        "NetworkError.\(self)".localized
    }
}

If we would like to cover unhandled future errors.

enum MainError: Error {
    case general
    case unknownError

    var userFriendlyDescription: String {
        "MainError.\(self)".localizeSafely(safe: "MainError.general".localized)
    }
}

extension String {

    var localized: String {
        NSLocalizedString(self, comment: "")
    }

    func localizeSafely(safe: String) -> String {
        let localizedValue = self.localized
        return localizedValue == self ? safe : localizedValue
    }
}

Assume that we have unknownError as a case but we did not create or want its localized value in the localizable file. We also don’t want the user sees something like MainError.unkownError. By using localizeSafely function, we can give an alternative text(the safe parameter) which will be used instead of the unhandled key.

Conclusion

Managing errors all over the app is easy peasy by creating the main enum with associated enums. Since the main enum can be extended, future error scenarios can be added and handled easily.