How do I use result types? When using a Result enum for callbacks, how to access the Error?
The most common Result
enumeration looks like this:
enum Result<Value> { case success(Value), failure(Error) }
A Result
is used almost exclusively in completion handlers. In synchronous code, it’s more common to use throwing functions rather than Result
types:
do { let value = try throwingFunction(...) } catch { ... handle error ... }
A Result
type replaces the (value: Value?, error: Error?) -> Void
handler signature used by many Cocoa APIs with this single Swift enumeration. Handling this type requires a slightly different approach than you’d use with thrown error handling.
As a rule, if an error is generated on your behalf, pay attention to it and don’t discard it out of hand. Errors help identify underlying issues that you may be able to resolve. They also provide important information for the developer and end-user of why an operation has failed.
The switch
statement provides the simplest approach to handle both result conditions with equal priority:
switch result { case .failure(let error): // handle error here case .success(let value): // handle success here }
If the error handling code is significantly less detailed than the success code, you might choose to perform early exit instead of using switch
. This approach allows you to handle any errors and then move on to processing the returned value at the top scope.
Use an if
statement (not a guard
statement) to bind error instances. Its primary clause should handle the bound error and then leave scope. If the result is success
, the if
-test will fail. Follow the error check with a guard
statement to bind the success value.
if case .failure(let error) = result { // handle error return } guard case .success(let value) = result else { fatalError("Success value could not be bound") } // this should never happen // use value
This second approach allows you to promote the typically detailed steps involved in processing a value after extracting it from the Result
enumeration. The guard
‘s else clause is a little ugly but necessary. Swift doesn’t offer a “forced enumeration unwrap” similar to Optional
‘s !
.
Breaking the handling down into an if
/guard
pair is not as elegant as the unified switch
statement, but it provides a practical way to promote the importance of the returned value.
Update: If the !!
operator is ever adopted, you could extend Result
to return a computed var value: Value?
member, and then use !!
instead of the guard/fatalError
combo in the above example to create a streamlined early return / value handling approach:
if case .failure(let error) = result { ... } let value = result.value !! "Success value could not be bound"
It’s a lot cleaner. See this PR for more details. (Thanks Dave)
6 Comments
I would almost always prefer:
Most error handling will either propagate the error somewhere or call specific error-handling routines, and in both of those cases it usually makes sense to pass things as the original result (of type Result but known to be failures) rather than doing the enumeration unwrap for failure cases all the time.
I.e. put the unwrapping of the failure case inside the “// handle error” if you have to, but often you don’t, or at least can funnel into much fewer places where you have to.
That would look like this:
Which is also kind of icky…
Sure, in those spots it’s icky. But most of the time it’ll be
// handle error:
return errorHandler(result)
I add a throwing ‘value()’ method to Result, so that the unpacking of the value can be performed in a do-catch:
{ result in
do {
let a = try result.value()
…use a…
} catch {
…handle error…
}
}
> Use an if statement (not a guard statement) to bind error instances.
Why rather use if than a guard statement?
Because you’re handling the error, not doing early return. It’s a true logic branch.