Optional Return vs throws: Pick One

Swift 2 provides a number of ways to indicate a problem in a method that returns a value, but the two most common are optional return values and throwing errors. Which you choose comes down to your requirements for a particular function, which I will discuss below. But if you take nothing else away from this post, take this: Don’t use both!

That is not to say you can’t use both approaches in your app, just don’t use them both in a single function.

Optional Returns

Pros: Simple to write, simple to handle
Cons: Uninformative

The most common method for indicating failure in a function that returns a value is to make that value optional and return nil when a problem occurs that prevents the successful completion of the function. This has been available since Swift 1.0 and the language has a lot of syntactical features to allow you to efficiently detect and handle nil responses.

Optional returns should be your first choice when all you care about is whether or not a value was returned from a function, not why the function may have failed.

Example:

import Swift
/// This method returns half of the input value if the number is even. It returns nil if the number is odd.
func evenHalfOrNil(input: Int) -> Int? {
if (input % 2 == 0) {
return input / 2
} else {
return nil
}
}
func test() {
// guard let handling
guard let halfOfFour = evenHalfOrNil(4) else {
print("It would seem that 4 is not even!?")
// Exit current scope.
return
}
print("halfOfFour:", halfOfFour)
// if let handling
if let _ = evenHalfOrNil(7) {
print("Huh! It seems like 7 is even. Who knew?")
} else {
print("As I expected! 7 is not even.")
}
// nil coalescing
let randomInt = Int(arc4random_uniform(10))
print("randomInt:", randomInt)
let evenOrZero = evenHalfOrNil(randomInt) ?? 0
print("evenOrZero:", evenOrZero)
}
test()
view raw OptionalReturn.swift hosted with ❤ by GitHub

Throwing Functions

Pros: Informative errors allow robust recovery options, non-optional return values
Cons: More verbose syntax to handle errors (if you don’t just ignore them with try?)

Swift 2 introduced a robust error handling mechanism to Swift. Functions marked with throws can throw errors which calling objects must explicitly handle or ignore. Having a well-defined set of errors allows calling objects to implement an intelligent recovery plan in the event of a failure, such as correcting erroneous input and trying again. The down-side is that writing error handling code gets verbose, with the do-catch blocks.

Example:

/// Our enumeration of error conditions.
enum NumberProcessingError: ErrorType {
case OddInput, NegativeInput
}
/// Accepts positive, even integers and returns half their value.
/// We're not using UInt because reasons.
func processNumber(input: Int) throws -> Int {
guard input >= 0 else {
throw NumberProcessingError.NegativeInput
}
guard input % 2 == 0 else {
throw NumberProcessingError.OddInput
}
return input / 2
}
func testProcessorWithOptionalTry() {
// Ignore the errors and treat them as nil returns.
if let halfThree = try? processNumber(-3) {
print("halfThree:", halfThree)
} else {
print("Something went wrong, but I don't care. On with the show!")
}
}
func testProcessorWithErrorHandling(testValue: Int) {
// Handle the errors.
print("Attempting to process:", testValue)
do {
let result = try processNumber(testValue)
print("Success! Here's the result:", result)
} catch NumberProcessingError.NegativeInput {
print("Negative input value. Consider using", abs(testValue))
} catch NumberProcessingError.OddInput {
print("The input must be even. Consider using", testValue + 1)
} catch {
print("An error occurred which we are not going to handle:", error)
}
}
testProcessorWithOptionalTry()
testProcessorWithErrorHandling(3)
testProcessorWithErrorHandling(-12)
testProcessorWithErrorHandling(4)

Combining Optional Returns and Errors

Don’t! If you don’t want to bother with errors, then your method should just return an optional value. If you do go to the effort of adding errors, let them communicate problems and guarantee a non-nil return value from your function. If the calling object doesn’t want to handle the errors, it can simply invoke your function with try? and treat the return value as optional. If the calling function does handle the errors, allow it to forgo the extra steps of handling optional return values.