Hands-On Design Patterns with Swift
上QQ阅读APP看书,第一时间看更新

Throwing and catching errors

Swift has an error handling mechanism that is based on throwing and catching errors, very similar to JavaScript or Java. In Swift, Error is simply a protocol. Any of your custom types conforming to the Error protocol can be used, and can also be thrown. This provides us with the ability to be as expressive as possible with the underlying types that we'll be using.

Unlike in Java, for example, it is not possible to specialize error catching, but you can always leverage pattern matching to determine the kind of error that's been thrown. You may want to define functions that throw, in order to indicate that an abnormal operation has run, and therefore, the execution of the program should properly handle these abnormal operations.

Let's look at our credit card example and suppose that we can create charges on a prepaid card:

class CreditCard {
private(set) var balance: Int // balance is in cents

init(balance: Int) {
self.balance = balance
}

func charge(amount: Int) {
balance -= amount
}
}

let card = CreditCard(balance: 10000)
card.charge(amount: 2000)
card.balance == 8000

This implementation is now completely unsafe, due to the following:

  • A developer can increment the balance by providing a negative amount
  • The balance can become negative, and we may want to include restrictions

We could use UInt instead of Int, but, for the sake of the example, we'll write our own error handling logic.

Let's rewrite our example, as follows:

class CreditCard {

Let's define ChargeError as an enum, which encompasses the two expected errors. Using an enum lets you safely define a finite possibility of values, while ensuring that the consumer will always implement all of the cases, and therefore, handle all of the error types:

    enum ChargeError: Error {
case invalidAmount
case insufficientFunds
}

/* unchanged implementation of init / balance */

We can now mark our charge method with the throws keyword, to indicate to anyone using this method that it can fail:

    func charge(amount: Int) throws {
guard amount >= 0 else { throw ChargeError.invalidAmount }
guard balance >= amount else { throw ChargeError.insufficientFunds }
balance -= amount
}
}

Let's take a look at how to use this API now:

The first way is to use the do...catch pattern and cast the error as the one thrown, as follows:

let card = CreditCard(balance: 10000)
do {
try card.charge(amount: 2000)
} catch CreditCard.ChargeError.invalidAmount {
/* handle invalidAmount */
} catch CreditCard.ChargeError.insufficientFunds {
/* handle insufficientFunds */
} catch {}

If you're not interested in catching errors, you can also use try?:

try? card.charge(amount: -1000) // this will fail safely and nicely

Error handling is a fundamental feature in Swift, when you need to interact with failable code. Throughout the course of this book, you'll see other patterns that encapsulate error management differently, and which many Swift users prefer over the default do...catch...throw pattern. However, for now let's continue our exploration of the Standard Library with the different container types.