SwiftErrorHandler enables expressing complex error handling logic with a few lines of code using a memorable fluent API.
pod 'SwiftErrorHandler', '~> 5.0'
github "stefanrenne/SwiftErrorHandler" ~> 5.0
import PackageDescription
let package = Package(
name: "My App",
dependencies: [
.package(url: "https://github.com/stefanrenne/SwiftErrorHandler.git", from: "5.0.0")
]
)
Let's say we're building a account based iOS app that can throw errors in the networking layer.
We need to:
The default ErrorHandler will contain the error handling logic that is common across your application that you don't want to duplicate. You can create a factory that creates it so that you can get new instance with common handling logic from anywhere in your app.
extension ErrorHandler {
class func `default`(for view: ErrorHandlerView) -> ErrorHandler {
return ErrorHandler(for: view)
.on(error: .code(NSURLErrorTimedOut), then: .present(alert: ConfirmableAlert(title: "Timeout occurred", confirmTitle: "Retry", confirmAction: { error in print("retry network call") })))
.on(error: .type(NetworkError.noInternet), then: .present(alert: ConfirmableAlert(title: "Did you turn off the internet?", confirmTitle: "No")))
.on(error: .type(NetworkError.logout), then: .present(alert: RejectableAlert(title: "Are you sure you want to logout?", confirmTitle: "Yes", rejectTitle: "No")))
.always(.perform(action: AnalyticsService.track))
.onNoMatch(.present(alert: ConfirmableAlert(title: "Something went wrong", confirmTitle: "Ok")))
}
}
Often the cases the default handler knows about will be good enough.
do {
try saveStatus()
} catch {
ErrorHandler.default(for: self).handle(error: error)
}
In cases where extra context is available you can add more cases or override the ones provided already.
For example in a LoginViewController
class LoginViewController: UIViewController {
private lazy var errorHandler = ErrorHandler.default(for: self)
.on(error: .type(NetworkError.authenticate), then: .perform(action: startAuthentication))
func performLogin() {
do {
try login()
} catch {
errorHandler.handle(error: error)
}
}
private func startAuthentication(for error: Error, onCompleted: OnErrorHandled) {
print("start authentication ...")
onCompleted?()
return true
}
}
let errorHandler = ErrorHandler.default(for: self)
Observable<User>
.error(NetworkError.authenticate)
.subscribe(onNext: { result in
print("User loggedin")
},
onError: errorHandler.handle)
.disposed(by: disposeBag)
let errorHandler = ErrorHandler.default(for: self)
let result: Result<User, NetworkError> = .failure(NetworkError.authenticate)
let user: User? = result.get(onError: errorHandler)
-
Performs actions for specific errors
errorHandler.on(error: .code(404), then: .present(Alert))
-
Performs actions when no specific error matcher can be found
errorHandler.onNoMatch(.present(Alert))
-
Actions that need to be performed for all errors
errorHandler.always(.perform(action: analyticsService.track))
Match on specific error type
errorHandler.on(error: .type(NetworkError.authenticate), then: .doNothing)
``errorHandler.on(error: .code(404), then: .doNothing)`
errorHandler.on(error: .domain("remote"), then: .doNothing)
extension ErrorMatcher {
static func onCustomMatch() -> ErrorMatcher {
.init(matcher: { error in
...
return true
})
}
}
.on(error: .onCustomMatch()), then: .doNothing)
It mainly exists to make documentation & unit tests easier to understand.
errorHandler.on(error: .code(404), then: .doNothing)
The Alert is presented on the View provided in the ErrorHandler init
errorHandler.on(error: .code(404), then: .present(alert: ErrorAlert))
By default there are two alert types you can present:
- ConfirmableAlert: An alert with one action button
- RejectableAlert: An alert with two action buttons
Would you like to use different alerts?
- Create a struct that conforms to the ErrorAlert protocol
- Implement the function that builds your custom UIAlertController
func build(for error: Error, onCompleted: OnErrorHandled) -> UIAlertController
- Make sure the optional
onCompleted
completionblock has been performed in allUIAlertAction
completion blocks
The only limitation is your mind.
errorHandler.on(error: .code(404), then: .perform(action: CustomActionHandler)
The CustomActionHandler provides the Error
and an optional onCompleted
completionblock that needs to be executed when your custom action has been performed.
In larger apps it makes sense to implement the ErrorHandler in a different class than the ViewController. To make this work you need to provide a view on which alerts can be presented. This can be done by conforming to the ErrorHandlerView protocol.
public protocol ErrorHandlerView {
func present(alert: UIAlertController)
}
extension UIViewController: ErrorHandlerView {
public func present(alert: UIAlertController) {
present(alert, animated: true, completion: nil)
}
}
Build your xcode project using the Swift Package Manager
swift package generate-xcodeproj --xcconfig-overrides ./Sources/ios.xcconfig
Quick checklist summary before submitting a PR
- 🔎 Make sure tests are added or updated to accomodate your changes. We do not accept any addition that comes without tests. When possible, add tests to verify bug fixes and prevent future regressions.
- 📖 Check that you provided a CHANGELOG entry documenting your changes (except for documentation improvements)
- 👌 Verify that tests pass
- 👍 Push it!
When designing for errors, we usually need to:
- have a default handler for expected errors // i.e. network, db errors etc.
- handle specific errors in a custom manner given the context of where and when they occur // i.e. a network error while uploading a file, invalid login
- have unspecific handlers that get executed on every error // i.e. log errors to Fabric or any other analytics service
- have a catch-all handler for unknown errors // i.e. errors we don't have custom handling for
- keep our code DRY
Swift has a well thought error handling model that balances between convenience (automatic propagation) and clarity-safety (Typed propagation, Marked propagation). As a result, the compiler warns of errors that need to be handled, while making it relatively easy to propagate errors and handle them higher up the stack.
However, even with this help from the language, achieving the goals listed above in an ad-hoc manner in an application of a reasonable size can lead to a lot of boilerplate which is tedious to write and reason about. Because of this friction, developers quite often choose to swallow errors or handle them all in the same generic way.
This library addresses these issues by providing an abstraction to define flexible error handling rules with an opinionated, fluent API.