I'm using a home made promise implementation with strongly typed errors throughout my code base, but my question is equally valid for Combine's publishers and just old plain Swift.Result.Failure
:
It is often the case that functions/publishers have a single well-defined error type, and that when combining two of more errors, one have to either erase the error types or create domain-specific combined errors such as:
enum NetworkError: Error {
case urlError(URLError.Code)
case httpError(status: Int)
...
}
enum APIError: Error {
case networkError(NetworkError)
case parsingError(DecodingError)
...
}
It allows me to ergonomically switch over cases such as:
APIClient.currentSession.fetchData().onCompletion { result in
switch result {
case .failure(networkError(.urlError(.timedOut))): // slow internet
case .failure(networkError(.urlError(.noConnection))): // no internet
case .failure(networkError): // other network errors.
case .failure(.httpError(status: .unauthorized)): // not logged in
case .failure(.httpError(status: 500..<599)): // server error
case .failure(.httpError(status: 400..<499)): // client error
case .failure(.parsingError): // invalid response
case .failure: // show generic error message
case .success(let response): // success!
}
}
However, if the above is part of yet another function call that may have other errors, the number of error types quickly adds up. I've been thinking about creating a generic Either
type that wraps two types into a .left
and .right
cases, and making it conform to Error
when both its associated types also conform:
public enum Either<Left, Right> {
case left(Left)
case right(Right)
}
APIClient.currentSession.fetchModel() // ← Promise<Model, Either<NetworkError, DecodingError>>
.flatMap(validateResponse) // ← Promise<Model, Either<Either<NetworkError, DecodingError>, ValidationError>
.map { $0.count } // ← Promise<Int, Either<Either<NetworkError, DecodingError>, ValidationError>
.onCompletion { result in
// is is possible to avoid switching over
// case .failure(.right(.left(.left(.networkError(.timedOut)))):
// and instead switch like this:
// case .failure(.networkError(.timedOut)):
// etc...?
}
I've tried this:
extension Either: Error where Left: Error, Right: Error {
public static func ~=(error: Self, pattern: Left) -> Bool {
if case .left(pattern) = error { return true }
return false
}
public static func ~=(error: Self, pattern: Right) -> Bool {
if case .right(pattern) = error { return true }
return false
}
}
But that doesn't work, since Referencing operator function '~=' on '_ErrorCodeProtocol' requires that 'Left' conform to '_ErrorCodeProtocol'
.
Also, even if I could get the pattern matching to work, I would still probably not get much help from the exclusivity checker, and it would probably take a toll on the type checker, too.
Any suggestions on how to get ergonomic aggregation of strong error types in a generic context? Is there a better way of dealing with this? I don't care if the actual error type is super contrived with many layers of wrapped generic types (like SwiftUI types), as long as the ergonomic of using them is not too bad. But perhaps there are better approaches to this?