Skip to content

Error Handler based on localized & healable (recoverable) errors without the overhead of NSError.

License

Notifications You must be signed in to change notification settings

JamitLabs/MungoHealer

Repository files navigation

Build Status Version: 0.3.2 Swift: 5.0 Platforms: iOS | tvOS License: MIT

InstallationUsageIssuesContributingLicense

MungoHealer

Error Handler based on localized & healable (recoverable) errors without the overhead of NSError (which you would have when using LocalizedError & RecoverableError instead).

Why use MungoHealer?

When developing a new feature for an App developers often need to both have presentable results fast and at the same time provide good user feedback for edge cases like failed network requests or invalid user input.

While there are many ways to deal with such situations, MungoHealer provides a straightforward and Swift-powered approach that uses system alerts for user feedback by default, but can be easily customized to use custom UI when needed.

tl;dr

Here's a very simple example of basic error handling without MungoHealer:

func login(success: (String) -> Void) {
    guard let username = usernameLabel.text, !username.isEmpty else {
        let alertCtrl = UIAlertController(title: "Invalid User Input", message: "Please enter a username.", preferredStyle: .alert)
        alertCtrl.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        viewController.present(alertCtrl, animated: true, completion: nil)
        return
    }
    guard let password = passwordLabel.text, !password.isEmpty else {
        let alertCtrl = UIAlertController(title: "Invalid User Input", message: "Please enter a password.", preferredStyle: .alert)
        alertCtrl.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        viewController.present(alertCtrl, animated: true, completion: nil)
        return
    }
    guard let apiToken = getApiToken(username, password) else {
        let alertCtrl = UIAlertController(title: "Invalid User Input", message: "Username and password did not match.", preferredStyle: .alert)
        alertCtrl.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        viewController.present(alertCtrl, animated: true, completion: nil)
        return
    }
    success(apiToken)
)

Using MungoHealer the above code becomes this:

func login(success: (String) -> Void) {
    mungo.do {
        guard let username = usernameLabel.text, !username.isEmpty else {
            throw MungoError(source: .invalidUserInput, message: "Please enter a username.")
        }
        guard let password = passwordLabel.text, !password.isEmpty else {
            throw MungoError(source: .invalidUserInput, message: "Please enter a password.")
        }
        guard let apiToken = getApiToken(username, password) else {
            throw MungoError(source: .invalidUserInput, message: "Username and password did not match.")
        }
        success(apiToken)
    }
)

Installation

Installing via Carthage & CocoaPods are both supported.

Support for SPM is currently not possible as this framework uses UIKit.

Usage

Please also have a look at the MungoHealer iOS-Demo project in the subfolder Demos for a live usage example.


Features Overview


Defining Errors

MungoHealer is based on Swifts built-in error handling mechanism. So before we can throw any useful error message when something goes wrong, we need to define our errors.

MungoHealer can deal with any errors thrown by system frameworks or third party libraries alike, but to make use of the user feedback automatics, you need to implement one of MungoHealers error type protocols:

BaseError

A localized error type without the overhead of NSError – truly designed for Swift. Use this for any error you want to provide localized user feedback for.

Requirements

source: ErrorSource A classification of the errors source. MungoHealer will automatically provide an alert title based on it. The available options are:

  • .invalidUserInput
  • .internalInconsistency
  • .externalSystemUnavailable
  • .externalSystemBehavedUnexpectedlyBased

The options are explained in detail here.

errorDescription: String A localized message describing what error occurred. This will be presented to the user as the alerts message by default when the error occurs.

debugDescription: String? An optional message describing the error in more technical detail for debugging purposes. This will not be presented to the user and so for only logged.

Example
struct PasswordValidationError: BaseError {
    let errorDescription = "Your password confirmation didn't match your password. Please try again."
    let source = ErrorSource.invalidUserInput
}

FatalError

A non-healable (non-recoverable) & localized fatal error type without the overhead of NSError – truly designed for Swift. Use this as an alternative for fatalError and tasks like force-unwrapping when you don't expect a nil value and therefore don't plan to heal (recover from).

Note that throwing a FatalError will crash your app, just like fatalError() or force-unwrapping nil would. The difference is, that here the user is first presented with an error message which is a better user experience. Additionally, before the app crashes you have the chance to do any cleanup or reporting tasks via a callback if you need to.

It is highly recommended to keep the suffix FatalError in your custom errors class name to clearly communicate that throwing this will crash the app.

Requirements

FatalError has the exact same requirements as BaseError. In fact its type declaration is as simple as this:

public protocol FatalError: BaseError {}

The only difference is semantics – the data provided by a FatalError will be used for alert title & message as well. But confirming the alert will crash the app.

Example
struct UnexpectedNilFatalError: FatalError {
    let errorDescription = "An unexpected data inconsistency has occurred. App execution can not be continued."
    let source = ErrorSource.internalInconsistency
}

HealableError

A healable (recoverable) & localized error type without the overhead of NSError – truly designed for Swift. Use this for any edge-cases you can heal (recover from) like network timeouts (healing via Retry), network unauthorized responses (healing via Logout) etc.

Requirements

HealableError extends BaseError and therefore has the same requirements. In addition to that, you need to add:

healingOptions: [HealingOption] Provides an array of possible healing options to present to the user. A healing option consists of the following:

  • style: Style: The style of the healing option. One of: .normal, .recommended or .destructive
  • title: String: The title of the healing option.
  • handler: () -> Void: The code to be executed when the user chooses the healing option.

Note that you must provide at least one healing option.

Example
struct NetworkUnavailableError: HealableError {
    private let retryClosure: () -> Void

    init(retryClosure: @escaping () -> Void) {
        self.retryClosure = retryClosure
    }

    let errorDescription = "Could not connect to server. Please check your internet connection and try again."
    let source = ErrorSource.externalSystemUnavailable

    var healingOptions: [HealingOption] {
        let retryOption = HealingOption(style: .recommended, title: "Try Again", handler: retryClosure)
        let cancelOption = HealingOption(style: .normal, title: "Cancel", handler: {})
        return [retryOption, cancelOption]
    }
}

Default Error Types

MungoHealer provides one basic implementation of each error protocol which you can use for convenience so you don't have to write a new error type for simple message errors. These are:

MungoError
  • Implements BaseError
  • init takes source: ErrorSource & message: String

Example Usage:

func fetchImage(urlPath: String) {
  guard let url = URL(string: urlPath) else {
    throw MungoError(source: .invalidUserInput, message: "Invalid Path")
  }

  // ...
}
MungoFatalError
  • Implements FatalError
  • init takes source: ErrorSource & message: String

Example Usage:

func fetchImage(urlPath: String) {
  guard let url = URL(string: urlPath) else {
    throw MungoFatalError(source: .invalidUserInput, message: "Invalid Path")
  }

  // ...
}
MungoHealableError
  • Implements HealableError
  • init takes source: ErrorSource & message: String
  • init additionally takes healOption: HealOption

Example Usage:

func fetchImage(urlPath: String) {
  guard let url = URL(string: urlPath) else {
    let healingOption = HealingOption(style: .recommended, title: "Retry") { [weak self] in self?.fetchImage(urlPath: urlPath) }
    throw MungoHealableError(source: .invalidUserInput, message: "Invalid Path", healingOption: healingOption)
  }

  // ...
}

Error Handling

MungoHealer makes handling errors easier by providing the ErrorHandler protocol and a default implementation of it based on alert views, namely AlertLogErrorHandler.

The easiest way to get started with MungoHealer is to use a global variable and set it in your AppDelegate.swift like this:

import MungoHealer
import UIKit

var mungo: MungoHealer!

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        configureMungoHealer()
        return true
    }

    private func configureMungoHealer() {
        let errorHandler = AlertLogErrorHandler(window: window!, logError: { print("Error: \($0)") })
        mungo = MungoHealer(errorHandler: errorHandler)
    }
}

Note that the following steps were taken in the above code:

  1. Add import MungoHealer at the top
  2. Add var mungo: MungoHealer! global variable
  3. Add a private configureMungoHealer() method
  4. Provide your preferred logError handler (e.g. SwiftyBeaver)
  5. Call configureMungoHealer() on app launch

As you can see, the AlertLogErrorHandler receives two parameters: The first is the window so it can find the current view controller to present alerts within. The second is a error log handler – the AlertLogErrorHandler not only presents alerts when there is a localized error, but it will also log all errors calling the logError handler with the errors localized description.

Custom ErrorHandler

While starting with the AlertLogErrorHandler is recommended by default, you might of course want to handle errors differently then just with system alerts & logs. For these cases, you just need to implement your own error handler by conforming to ErrorHandler which requires the following methods:

  • handle(error: Error): Called for "normal" error types.
  • handle(baseError: BaseError): Called for base error types.
  • handle(fatalError: FatalError): Called for fatal error types – App should crash at the end of this method.
  • handle(healableError: HealableError): Calles for healable error types.

See the implementation of AlertLogErrorHandler here for a working example.

Note that you don't have to use a single global variable named mungo as in the example above. You could also write your own Singleton with multiple MungoHealer objects, each with a different ErrorHandler type. This way you could choose to either show an alert or your custom handling, depending on the context. The Singleton might look something like this:

enum ErrorHandling {
    static var alertLogHandler: MungoHealer!
    static var myCustomHandler: MungoHealer!
}

Usage Example

Once you've written your own error types and configured your error handler, you should write throwing methods and deal with errors either directly (as before) or use MungoHealer's handle method which will automatically deal with the error cases.

Here's a throwing method:

private func fetchImage(urlPath: String) throws -> UIImage {
    guard let url = URL(string: urlPath) else {
        throw StringNotAValidURLFatalError()
    }

    guard let data = try? Data(contentsOf: url) else {
        throw NetworkUnavailableError(retryClosure: { [weak self] intry self?.loadAvatarImage() })
    }

    guard let image = UIImage(data: data) else {
        throw InvalidDataError()
    }

    return image
}

You can see that different kinds of errors could be thrown here. All of them can be handled at once as easy as this:

private func loadAvatarImage() {
    do {
        imageView.image = try fetchImage(urlPath: user.avatarUrlPath)
    } catch {
        mungo.handle(error)
    }
}

We don't need to deal with error handling on the call side which makes our code both more readable & more fun to write. Instead, we define how to deal with the errors at the point where the error is thrown/defined. On top of that, the way errors are communicated to the user is abstracted away and can be changed App-wide by simply editing the error handler code. This also makes it possible to handle errors in the model or networking layer without referencing any UIKit classes.

For cases where you just want one catch-all where you just call the handle(error) method, there's even a shorthand which will deal with this automatically. Just use this instead of the above code:

private func loadAvatarImage() {
    mungo.do {
        imageView.image = try fetchImage(urlPath: user.avatarUrlPath)
    }
}

So as you can see, used wisely, MungoHealer can help to make your code cleaner, less error prone and it can improve the User Experience for your users.

Contributing

See the file CONTRIBUTING.md.

License

This library is released under the MIT License. See LICENSE for details.