One of the most awaited proposals for Swift 5, Result, has already landed into the language. The Result
type forces the programmer to explicitly handle the failure and success cases before they can gain access to the actual value. Let’s have a look at how it is implemented, how you can use it, and why it was needed.
The Result
type is meant to enforce a safe way to handle errors returned by a function call without resorting to exceptions. While exceptions provide an automatic mechanism for error propagation and handling, Result
provides a manual mechanism with stronger type guarantees and more suitable to async error handling. Similarly to the Option
type, the Result
type is a monad.
The Result type in Swift 5 is implemented as an enum
with two cases: .success
and .failure
.
public enum Result<Success, Failure: Error> {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
...
}
You can define a simple function returning a Result
and handle its result like this:
enum AnErrorType: Error {
case failureReason1
case failureReason2
}
func failableFunction() -> Result<Int, AnErrorType> {
...
if (errorCondition1) {
return .failure(.failureReason1)
}
...
return .success(1)
}
func callFailableFunction() {
...
let result = failableFunction()
switch result {
case .success(let integerResult):
...
case .failure(let error):
switch error {
case .failureReason1:
...
case .failureReason2:
...
}
}
...
}
Additionally, to make it easier to integrate with existing functions, the Result
type supports a specific initializer that accepts a closure that may throw:
let result = Result { try String(contentsOfFile: configuration) }
Being a monad, Result
implements the known map()
and flatMap()
methods (along with mapError()
and flatMapError()
which are conceptually the same):
-
map()
(mapError()
) enables the automatic transformation of a value (error) through a closure, but only in case of success (failure), otherwise theResult
is left unmodified. -
flatMap()
(flatMapError()
) is useful when you want to transform your value (error) using a closure that returns itself aResult
to handle the case when the transformation fails. In such cases,map()
(mapError()
) would return a<Result<Result<...>>
. WithflatMap()
(flatMapError()
), you get a simple, flattenedResult
instead.
Result
types represent an improvement over the do
try
catch
throw
syntax which is the basic mechanism for error handling in Swift since Swift 2 for a number of reasons.
First and foremost, using Result types it becomes so much more natural to handle async failures. A typical pattern to handle async function calls in Swift makes use of callbacks, like in the following example:
asyncOperationCall() { (value, error) in
guard error != nil else { self.handleError(error!) }
self.handleValue(value)
}
As you may easily recognize, using callbacks defeats the purpose of exceptions in Swift, which is automatic propagation and handling of errors. Instead, with async callbacks, error must be handled in place, because when an exception may be thrown from a callback it is already too late for the error to be automatically propagated up the call stack. On the contrary, a Result
may be stored and processed at a later point when someone else attempts to use the returned value. This property, called delayed error handling, is not specific to async code and may greatly simplify Swift syntax, as shown in the following example:
do {
handleOne(try String(contentsOfFile: oneFile))
} catch {
handleOneError(error)
}
do {
handleTwo(try String(contentsOfFile: twoFile))
} catch {
handleTwoError(error)
}
do {
handleThree(try String(contentsOfFile: threeFile))
} catch {
handleThreeError(error)
}
The code above translates into the following much more readable snippet in Swift 5:
let one = Result { try String(contentsOfFile: oneFile) }
let two = Result { try String(contentsOfFile: twoFile) }
let three = Result { try String(contentsOfFile: threeFile) }
handleOne(one)
handleTwo(two)
handleThree(three)
Another advantage of Result
is that we know for sure we either get an error or a value. When using a callback, we have actually four different combinations that we should think about: a good value with no error; no value with an error; a value with an error; no value and no error.
Finally, using a Result
type enables constraining the exact error type that may be returned. In the first example we gave above, this property allowed us to fully enumerate the possible error causes, i.e., .failureReason1
and .failureReason2
, along the error handling path. On the contrary, a Swift function that can throw does not specify the type that can be thrown, so when handling the error condition in a catch
block, you only know your error conforms to the Error
protocol.
Swift 5 is planned to be released early 2019 and focuses on bringing ABI stability to the language.