My use case: I am performing 3 or more operations in a row. Each operation can throw an error and I want to properly propagate these errors up the chain.
enum OperationError: Error {
case anything
}
enum LargeOperationError: Error {
case parseFailureField(field: String, message: String)
}
func performLargeOperation() -> Result<T, LargeOperationError> {
let result1 = operation1()
// if result1 is .failure, return a new .failure(.parseFailureField("op 1", result1's error description))
// else, continue in the same scope with the .success variable
let result2 = operation2()
// if result2 is .failure, return a new .failure(.parseFailureField("op 2", result2's error description))
// else, continue in the same scope with the .success variable
let result3 = operation3()
// if result3 is .failure, return a new .failure(.parseFailureField("op 3", result1's error description))
// else, continue in the same scope with the .success variable
// do something with the success results of the above three operations
}
My real-world use case for this is deserializing fixed width integers off of a network byte stream and constructing a packet object with the information.
Approach 1: Do/get/catch
Here, we can use .get() throws
of the Result<Success, Failure>
and catch everything and throw a new error from there.
do {
let value1 = try operation1().get()
let value2 = try operation2().get()
let value3 = try operation3().get()
return .success(T(value1, value2, value3))
} catch is OperationError {
return .failure(.parseFailureField("???", error.description))
}
The problem here is that we don't know which operation of the three failed. The failure we can return can propagate the information of the originally returned failure (error.description
), but the calling code would not know which part failed in here.
Approach 2: guard
Here, we can use guard statements to capture the information of which operation failed.
let result1 = operation1()
guard case let .success(value1) = result1 else {
return .failure(.parseFailureField("operation 1", "???"))
}
let result2 = operation2()
guard case let .success(value2) = result2 else {
return .failure(.parseFailureField("operation 2", "???"))
}
let result3 = operation3()
guard case let .success(value3) = result3 else {
return .failure(.parseFailureField("operation 3", "???"))
}
return return .success(T(value1, value2, value3))
We've sacrificed our error propagation for our ability to provide detailed, specific error reporting. We can report which of the parts within our larger operation has failed which aids in debugging, but we don't have information about what caused the error anymore.
Approach 3: nested switch
This is the safest way to properly handle everything as needed.
let result1 = operation1()
switch result1 {
case let .failure(err):
return .failure(.parseFailureField("operation 1", err.description))
case let .success(value1):
let result2 = operation2()
switch result2 {
case let .failure(err):
return .failure(.parseFailureField("operation 2", err.description))
case let .success(value2):
let result3 = operation3()
switch result3 {
case let .failure(err):
return .failure(.parseFailureField("operation 3", err.description))
case let .success(value3):
return return .success(T(value1, value2, value3))
}
}
}
This follows the "return early" pattern [0], but it's nested and isn't linear. We can improve this somewhat like so.
let result1 = operation1()
let value1: T
switch result1 {
case let .failure(err):
return .failure(.parseFailureField("operation 1", err.description))
case let .success(_value1):
value1 = _value1
}
let result2 = operation2()
let value2: T
switch result2 {
case let .failure(err):
return .failure(.parseFailureField("operation 2", err.description))
case let .success(_value2):
value2 = _value2
}
let result3 = operation3()
let value3: T
switch result3 {
case let .failure(err):
return .failure(.parseFailureField("operation 3", err.description))
case let .success(_value3):
value3 = _value3
}
return return .success(T(value1, value2, value3))
We now have both propagation of the original error, as well as more specific error handling. But wow, look at all of the code we need to add for something that should be a lot simpler. This is unfortunately the best solution I've found so far.
Approach 4: the forbidden !
operator
We can vastly simplify the code, at the expense of safety.
let result1 = operation1()
guard case let .success(value1) = result1 else {
return .failure(.parseFailureField("operation 1", result1.error!.description))
}
let result2 = operation2()
guard case let .success(value2) = result2 else {
return .failure(.parseFailureField("operation 2", result2.error!.description))
}
let result3 = operation3()
guard case let .success(value3) = result3 else {
return .failure(.parseFailureField("operation 3", result3.error!.description))
}
return return .success(T(value1, value2, value3))
extension Result {
var error: Failure? {
switch self {
case let .failure(err):
return err
case .success(_):
return nil
}
}
}
But if someone messes up the code somehow, we suddenly get unexpected force unwraps of the nil .error
property in the extension, which is something to avoid.
Discussion
It's at this point that I'm not sure how best to move forward. My goals would be to minimize the amount of boilerplate code, to handle both propagation and field-specific handling as outlined above, and to avoid unsafe practices such as using the force unwrap operator.
Is this where using Result
's .map
, .flatMap
, .mapError
, and/or .flatMapError
functions would come into play, possibly?
Some guidance on how to handle errors with these constraints would be appreciated!
Possible proposal
Pondering the above, I had the idea of a change to the guard
statement to make this all easier. Essentially, while the guard case let
binds a variable to the scope outside of the block, if it's binding to a Result<Success, Failure>
(or possibly any enum with two cases), there could be a new bound variable on the inside of the guard statement of the other case. This would essentially be syntactical sugar to Approach 3. If this post does not yield sufficient results, I may make a separate post outlint this in more detail under the Evolution category of the forum.