Sam from the Domain iOS team here with a rundown of why you should be using the awesome power of the Swift enum to represent the results of your failable operations. Hold tight, this is going to get Schwifty.

The problem

If you’re writing an app which makes network or database calls, chances are you have something like this:

BananaDataSource - pre-Result import BananaKit import FruitNetworking struct BananaDataSource { typealias FetchCompletion = (banana: Banana?, error: NSError?) -> Void static func fetchBananaWithURL(URLString: String, completion: FetchCompletion) { guard let URL = NSURL(string: URLString) else { completion(banana: nil, error: self.invalidURLError(URLString)) return } let requestOperation = FruitHTTPRequestOperation(request: NSURLRequest(URL: URL)) requestOperation.responseSerializer = FruitJSONResponseSerializer() requestOperation.setCompletionBlockWithSuccess({ operation, responseObject in guard let banana = BananaModelFactory.bananaWithDict(responseObject) else { completion(banana: nil, error: self.bananaDeserialisingError()) return } completion(banana: banana, error: nil) }, failure: { operation, error in completion(banana: nil, error: error) }) requestOperation.start() } //MARK: Errors static let errorDomain = "au.com.domain.BananaDataSource" static let deserialisingErrorCode = 0 static let invalidURLErrorCode = 1 private static func bananaDeserialisingError() -> NSError { return NSError(domain: self.errorDomain, code: self.deserialisingErrorCode, userInfo: [NSLocalizedDescriptionKey: "BananaDataSource couldn't deserialise"]) } private static func invalidURLError(URLString: String) -> NSError { return NSError(domain: self.errorDomain, code: self.invalidURLErrorCode, userInfo: [NSLocalizedDescriptionKey: "invalid URL provided to BananaDataSource \(URLString)"]) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import BananaKit import FruitNetworking struct BananaDataSource { typealias FetchCompletion = ( banana : Banana ? , error : NSError ? ) -> Void static func fetchBananaWithURL ( URLString : String , completion : FetchCompletion ) { guard let URL = NSURL ( string : URLString ) else { completion ( banana : nil , error : self . invalidURLError ( URLString ) ) return } let requestOperation = FruitHTTPRequestOperation ( request : NSURLRequest ( URL : URL ) ) requestOperation . responseSerializer = FruitJSONResponseSerializer ( ) requestOperation . setCompletionBlockWithSuccess ( { operation , responseObject in guard let banana = BananaModelFactory . bananaWithDict ( responseObject ) else { completion ( banana : nil , error : self . bananaDeserialisingError ( ) ) return } completion ( banana : banana , error : nil ) } , failure : { operation , error in completion ( banana : nil , error : error ) } ) requestOperation . start ( ) } //MARK: Errors static let errorDomain = "au.com.domain.BananaDataSource" static let deserialisingErrorCode = 0 static let invalidURLErrorCode = 1 private static func bananaDeserialisingError ( ) -> NSError { return NSError ( domain : self . errorDomain , code : self . deserialisingErrorCode , userInfo : [ NSLocalizedDescriptionKey : "BananaDataSource couldn't deserialise" ] ) } private static func invalidURLError ( URLString : String ) -> NSError { return NSError ( domain : self . errorDomain , code : self . invalidURLErrorCode , userInfo : [ NSLocalizedDescriptionKey : "invalid URL provided to BananaDataSource \ ( URLString ) " ] ) } }

with a consumer that looks like:

getBanana - pre-Result func getBanana() { BananaDataSource.fetchBananaWithURL(self.currentBananaURLString) { banana, error in if let error = error where error.domain == BananaDataSource.errorDomain { ErrorLogging.logError(error) if error.code == BananaDataSource.invalidURLErrorCode { self.flagBrokenURL(self.currentBananaURLString) } } else if let error = error { ErrorLogging.logError(error) self.retryGetBanana() } else if let banana = banana { self.peelBanana(banana) } else { //Impossible, right? } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func getBanana ( ) { BananaDataSource . fetchBananaWithURL ( self . currentBananaURLString ) { banana , error in if let error = error where error . domain == BananaDataSource . errorDomain { ErrorLogging . logError ( error ) if error . code == BananaDataSource . invalidURLErrorCode { self . flagBrokenURL ( self . currentBananaURLString ) } } else if let error = error { ErrorLogging . logError ( error ) self . retryGetBanana ( ) } else if let banana = banana { self . peelBanana ( banana ) } else { //Impossible, right? } } }

It does the job, but it’s far from perfect:

We’ve assumed in writing our completion that banana and error are mutually exclusive, but there’s nothing in the completion signature to tell us that. To know for sure that it’s impossible to get both an error and a banana back (or neither), we need to look at the BananaDataSource code.

and are mutually exclusive, but there’s nothing in the completion signature to tell us that. To know for sure that it’s impossible to get both an and a back (or neither), we need to look at the code. We want to flag the URL as broken if we pick up an invalidURLErrorCode , but it wouldn’t be sensible to pack the offending URL in the NSError and then extract it later, so we use self.currentBananaURLString . But what if currentBananaURLString has changed in between making the request and receiving the response. We’ve inadvertently reported the incorrect URL.

, but it wouldn’t be sensible to pack the offending URL in the and then extract it later, so we use . But what if has changed in between making the request and receiving the response. We’ve inadvertently reported the incorrect URL. We know about invalidURLErrorCode and we’re handling it accordingly, but what if someone maintaining BananaDataSource introduces a new error and forgets to consider how our consumer might handle that?

and we’re handling it accordingly, but what if someone maintaining introduces a new error and forgets to consider how our consumer might handle that? It’s implicit in our handling that if the error.domain != BananaDataSource.errorDomain , we’re dealing with a networking error of some sort, but that’s not obvious to anyone reading the code for the first time.

Enter Result

One solution to these problems is to use an enum to represent all of the possible completion states of the fetchBanana operation. Swift enums are a powerful construct in many ways but here we’ll specifically use their ability to hold associated values.

We Antitypical Result framework for this task. It provides much excellent functionality to help reduce boilerplate and make Result-y code more elegant and expressive, but you can basically reduce it down to a few lines:

enum Result<Value, Error> { case Success(Value) case Failure(Error) } 1 2 3 4 enum Result < Value , Error > { case Success ( Value ) case Failure ( Error ) }

Result is just an enum with two generic parameters. So when I define a Result type of, say, Result<Banana, NSError> what I’m doing is defining a contract for any operation with that result. I’m saying that the operation must return either a .Success containing a Banana , or a .Failure containing an NSError . There’s no middle ground – if you try to return .Success(nil) or return .Failure(somethingThatIsntAnNSError) you’ll get a compile error. This allows us to express the mutual exclusivity of Banana and NSError via the type system. Where before we had a piece of knowledge that was shared between two objects and only checked at runtime, we now have a compile-time enforcement.

We get a similar enforcement in the code which consumes and handles the Result :

typealias BananaResult = Result<Banana, NSError> func fetchBanana() -> BananaResult { ... } ... switch fetchBanana() { case let .Success(banana): peelBanana(banana) case let .Failure(error): logError(error) } 1 2 3 4 5 6 7 8 9 10 11 12 typealias BananaResult = Result < Banana , NSError > func fetchBanana ( ) -> BananaResult { ... } ... switch fetchBanana ( ) { case let . Success ( banana ) : peelBanana ( banana ) case let . Failure ( error ) : logError ( error ) }

In the block of code above we’ve elegantly and exhaustively handled all possible outcomes of fetchBanana . The switch statement above doesn’t have a default statement because the compiler can verify for us that we’ve handled all possible cases.

Fixing the DataSource

So refactoring the BananaDataSource example above to use Result , we get something like this:

BananaDataSource - post-Result import BananaKit import FruitNetworking import Result struct BananaDataSource { enum FetchError: ErrorType { case InvalidURL(URLString: String) case NetworkFailed(error: NSError) case DeserialisingFailed } typealias FetchResult = Result<Banana, FetchError> typealias FetchCompletion = (result: FetchResult) -> Void static func fetchBananaWithURL(URLString: String, completion: FetchCompletion) { guard let URL = NSURL(string: URLString) else { completion(result: .Failure(.InvalidURL(URLString: URLString))) return } let requestOperation = FruitHTTPRequestOperation(request: NSURLRequest(URL: URL)) requestOperation.responseSerializer = FruitJSONResponseSerializer() requestOperation.setCompletionBlockWithSuccess({ operation, responseObject in guard let banana = BananaModelFactory.bananaWithDict(responseObject) else { completion(result: .Failure(.DeserialisingFailed)) return } completion(result: .Success(banana)) }, failure: { operation, error in completion(result: .Failure(.NetworkFailed(error: error))) }) requestOperation.start() } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import BananaKit import FruitNetworking import Result struct BananaDataSource { enum FetchError : ErrorType { case InvalidURL ( URLString : String ) case NetworkFailed ( error : NSError ) case DeserialisingFailed } typealias FetchResult = Result < Banana , FetchError > typealias FetchCompletion = ( result : FetchResult ) -> Void static func fetchBananaWithURL ( URLString : String , completion : FetchCompletion ) { guard let URL = NSURL ( string : URLString ) else { completion ( result : . Failure ( . InvalidURL ( URLString : URLString ) ) ) return } let requestOperation = FruitHTTPRequestOperation ( request : NSURLRequest ( URL : URL ) ) requestOperation . responseSerializer = FruitJSONResponseSerializer ( ) requestOperation . setCompletionBlockWithSuccess ( { operation , responseObject in guard let banana = BananaModelFactory . bananaWithDict ( responseObject ) else { completion ( result : . Failure ( . DeserialisingFailed ) ) return } completion ( result : . Success ( banana ) ) } , failure : { operation , error in completion ( result : . Failure ( . NetworkFailed ( error : error ) ) ) } ) requestOperation . start ( ) } }

Note that the Error type for FetchResult is itself an enum . By nesting an error enum within the result enum , we can represent our failure outcomes expressively and concisely. For example, the invalid URL guard block:

guard let URL = NSURL(string: URLString) else { let result = FetchError.Failure(.InvalidURL(URLString: URLString)) completion(result: result) return } 1 2 3 4 5 guard let URL = NSURL ( string : URLString ) else { let result = FetchError . Failure ( . InvalidURL ( URLString : URLString ) ) completion ( result : result ) return }

…we’ve packed the cause of the problem in a statically-typed, clearly named structure.

There’s not much we can do about the fact that the FruitNetworking library will give us an NSError on failure, but we can use the same approach of packing the error in a .Failure(.NetworkFailed(error: error)) to help the consumer differentiate between BananaDataSource ‘s error states and those of the networking library.

Fixing the consumer

If we refactor getBanana accordingly, we see more benefits:

getBanana - post-Result func getBanana() { BananaDataSource.fetchBananaWithURL(self.currentBananaURLString) { result in switch result { case let .Success(banana): self.peelBanana(banana) case let .Failure(.InvalidURL(URLString)): ErrorLogging.logErrorWithMessage("invalid URL provided to BananaDataSource \(URLString)") self.flagBrokenURL(URLString) case let .Failure(.NetworkFailed(error)): ErrorLogging.logErrorWithMessage("BananaDataSource failed to fetch with error: \(error)") case .Failure(.DeserialisingFailed): ErrorLogging.logErrorWithMessage("BananaDataSource failed to deserialise") } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func getBanana ( ) { BananaDataSource . fetchBananaWithURL ( self . currentBananaURLString ) { result in switch result { case let . Success ( banana ) : self . peelBanana ( banana ) case let . Failure ( . InvalidURL ( URLString ) ) : ErrorLogging . logErrorWithMessage ( "invalid URL provided to BananaDataSource \ ( URLString ) " ) self . flagBrokenURL ( URLString ) case let . Failure ( . NetworkFailed ( error ) ) : ErrorLogging . logErrorWithMessage ( "BananaDataSource failed to fetch with error: \ ( error ) " ) case . Failure ( . DeserialisingFailed ) : ErrorLogging . logErrorWithMessage ( "BananaDataSource failed to deserialise" ) } } }

All of the outcomes of fetchBananaWithURL are handled in a single exhaustive switch statement. The Result and FetchError cases serve to comment those outcomes – the happy .Success path is more visible and the cause of each of the .Failure cases is now obvious.

are handled in a single exhaustive statement. The and cases serve to comment those outcomes – the happy path is more visible and the cause of each of the cases is now obvious. Associated values for each case are statically typed, and we’ve dropped the error-prone use of self.flagBrokenURL(self.currentBananaURLString) .

. If someone were to add an extra case to BananaDataSource.FetchError , for example, we’d get a compiler error in getBanana until we added appropriate handling of that case.

Extending FetchError

The code above doesn’t necessarily give us all of the information we need about the causes of each failure, but fortunately enum cases can contain multiple associated values:

enum FetchError: ErrorType { case InvalidURL(URLString: String) case NetworkFailed(URLString: String, error: NSError) case DeserialisingFailed(URLString: String) } ... let deserialiseFailure = FetchError.Failure(.DeserialisingFailed(URLString: URLString)) let networkFailure = FetchError.Failure(.NetworkFailed(URLString: URLString, error: error)) 1 2 3 4 5 6 7 8 9 enum FetchError : ErrorType { case InvalidURL ( URLString : String ) case NetworkFailed ( URLString : String , error : NSError ) case DeserialisingFailed ( URLString : String ) } ... let deserialiseFailure = FetchError . Failure ( . DeserialisingFailed ( URLString : URLString ) ) let networkFailure = FetchError . Failure ( . NetworkFailed ( URLString : URLString , error : error ) )

func getBanana() { BananaDataSource.fetchBananaWithURL(self.currentBananaURLString) { result in switch result { case let .Success(banana): self.peelBanana(banana) case let .Failure(.InvalidURL(URLString)): ErrorLogging.logErrorWithMessage("invalid URL provided to BananaDataSource \(URLString)") self.flagBrokenURL(URLString) case let .Failure(.NetworkFailed(URLString, error)): ErrorLogging.logErrorWithMessage("BananaDataSource failed to fetch for \(URLString), error: \(error)") case .Failure(.DeserialisingFailed(URLString)): ErrorLogging.logErrorWithMessage("BananaDataSource failed to deserialise for \(URLString)") } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func getBanana ( ) { BananaDataSource . fetchBananaWithURL ( self . currentBananaURLString ) { result in switch result { case let . Success ( banana ) : self . peelBanana ( banana ) case let . Failure ( . InvalidURL ( URLString ) ) : ErrorLogging . logErrorWithMessage ( "invalid URL provided to BananaDataSource \ ( URLString ) " ) self . flagBrokenURL ( URLString ) case let . Failure ( . NetworkFailed ( URLString , error ) ) : ErrorLogging . logErrorWithMessage ( "BananaDataSource failed to fetch for \ ( URLString ) , error: \ ( error ) " ) case . Failure ( . DeserialisingFailed ( URLString ) ) : ErrorLogging . logErrorWithMessage ( "BananaDataSource failed to deserialise for \ ( URLString ) " ) } } }

Further reading