An error handling reactive extension for fine-grained control over Abort, Retry, Fail?
Battleship (also Battleships or Sea Battle) is a guessing game for two players. It is played on ruled grids (paper or board) on which each player's fleet of ships (including battleships) are marked. The locations of the fleets are concealed from the other player. Players alternate turns calling "shots" at the other player's ships, and the objective of the game is to destroy the opposing player's fleet.
SinkEmAll extends the way of handling errors in RxSwift. It brings (A)bort, (R)etry, (F)ail? pattern back to the game. Yes, that's the battleship game!
/// Definition of a single attempt of handling error.
/// That's the battleship game: you can miss, hit, or sink on target.
///
/// - miss: Attempt the operation again. (R)etry
/// - hit: Throw an error to the next handler in the chain of responsibility. (A)bort or (I)gnore
/// - sink: Gracefully recover from an error. (F)ail
public enum Shot {
case miss
case hit(Error)
case sink
}
Therefore, you can implement ErrorShooting
protocol in order to take a single shot on target.
extension ViewController: ErrorShooting {
func shoot(error: RetriableError, attempt: Int, complete: @escaping (Shot) -> Void) {
let alert = UIAlertController(title: "", message: "Something wrong happened", preferredStyle: .alert)
if error.canRetry && attempt < 3 {
alert.addAction(
UIAlertAction(title: "Retry", style: .cancel) { _ in
complete(.miss)
}
)
}
alert.addAction(
UIAlertAction(title: "Rethrow", style: .destructive) { _ in
complete(.hit(error))
}
)
alert.addAction(
UIAlertAction(title: "Close", style: .default) { _ in
complete(.sink)
}
)
present(alert, animated: true)
}
}
Take note that shoot function is generic so another error type can be used.
class ConsoleErrorLogger: ErrorShooting {
public let level: Level
public init(level: Level) {
self.level = level
}
public func shoot(error: DescribableError, attempt: Int, complete: ((Shot) -> Void)) {
if let message = error.message(for: level) {
print("Error Log: \(message)")
}
complete(.hit(error))
}
}
After that you can use your instance as a shooter for reactive operator.
observableThatErrorsOut
.shootError(with: viewController)
.shootError(with: ConsoleErrorLogger(level: .debug))
Another example with custom shooter function:
observableThatErrorsOut
.shootError { error, _ in
print("Sinking an error: \(error.localizedDescription)")
return .just(.sink)
}
Please look at ViewController.swift
file in example project for more examples.
To run the example project, clone the repo and run pod install
from the Example directory first.
- Replace completion handlers with dedicated shot observable
- Add miss, hit and sink hooks to error traits
- Wrap around the underlying error in some ship struct
- What about Result type?
- Add support for tvOS, watchOS and macOS deployment targets
- Add support for other reactive frameworks
- Add timeout
- Provide more examples
- Improve README.md by adding a diagram of shooting an error
- How to enjoy error handling in RxSwift
Piotr Boguslaw Łyczba, [email protected]
SinkEmAll is available under the MIT license. See the LICENSE file for more info.