Error handling with return values

As we mentioned earlier, error handling, in its most simplest form, uses the return value from a function or method to indicate whether it was successful or not. This return value could be something as simple as a Boolean true/false value or something more complex such as an enumeration, whose values indicate what actually went wrong if the function or method was unsuccessful. This was one of the primary error-handling patterns with Objective-C and also with Swift prior to version 2.0. This is still a very valid pattern if we just need something to indicate success or failure.

Let's see how we would use the return value to add error handling to the drinking() method of the Drink structure. For this function, we just need an indicator that will let us know if we were able to successfully take the drink. This is the ideal situation for using a Boolean return value. A return value of true will indicate that the function was successful and a return value of false will indicate that it failed. Let's see how we would implement this with the drinking() method:

mutating func drinking(amount: Double) -> Bool {
    guard amount <= volume else {
        return false
    }
    volume -= amount
    return true
}

In this method, we use the guard statement to verify that the amount that we want to drink is less than or equal to the volume remaining in the drink. If not, we return a false value indicating that the method failed; otherwise, we subtract the amount we drank from the volume remaining in the drink and return a true value, indicating that the method was successful.

Adding a simple Boolean return value to our methods is one of the easiest ways to add error handling to our applications. It is also very easy to check for errors with this type of error handling. For example the following code creates an instance of the Drink class and then uses the drinking() method to take a drink. We then verify that we successfully took a drink:

var myDrink = Drink(volume: 23.5, caffeine: 280,
    temperature: 38.2, drinkSize: DrinkSize.Can24,
    description: "Drink Structure")

if myDrink.drinking(50.0) {
    print("Had a drink")
} else {
    print("Error")
}

In this code, to check for errors all we needed to do was to call to the drinking() method in an if statement to verify that the method was successful.

Now let's look at how we would use a return value to indicate whether the temperature of the drink is still in the acceptable range after a call to the temperatureChange() method. The return value for the method will be an enumeration that will let us know if the temperature of the drink is too cold, too hot, or just right. This enumeration will look like this:

enum DrinkTemperature {
    case TooHot
    case TooCold
    case JustRight
}

Now let's see how we would change the temperatureChange() method to let us know whether the temperature of the drink is within the acceptable range:

mutating func temperatureChange(change: Double) -> DrinkTemperature {
    temperature += change
    guard temperature >= 35 else {
        return .TooCold
    }
    guard temperature <= 45 else {
        return .TooHot
    }
    return .JustRight
}

The acceptable range of temperature for our drinks is between 35 and 45 degrees; therefore in our code we need to verify that the temperature of the drink is greater than or equal to 35 degrees and less than or equal to 45 degrees. In our new method we use a guard statement to verify that the drink is greater than or equal to 35 degrees; otherwise we return a TooCold value. We then use another guard statement to verify that the drink is less than or equal to 45 degrees; otherwise we return a TooHot value. If the temperature successfully passes the two guard statements, we return a JustRight value.

We would use this function like this:

var results = myDrink.temperatureChange(-5)
switch results {
case .TooHot:
    print("Drink too hot")
case .TooCold:
    print("Drink too cold")
case .JustRight:
    print("Drink just right")
}

In this example we used a switch statement after we changed the temperature to see if the temperature of the drink got too hot, too cold, or was still in the acceptable range. We could use an if statement if all we wanted to do was to verify that the temperature was in an acceptable range.

If we needed additional information about our error, we could use associated values with our enumeration. In our example, this additional information could be the actual temperature of the drink.

What we saw in this section is the easiest error-handling pattern to implement; however, there are several drawbacks to this pattern. The biggest drawback to this error-handling pattern is that it is by far the easiest form to ignore. It is very easy, in our code, to call these functions and then ignore the returned value. This can cause issues in our application if we simply assume a function/method was successful when it isn't.

As a general rule, if a function/method has a return value we should always check it to verify that the function completed successfully. The next error handling pattern that we will look at is how we would use the NSError class to return detailed information about an error.