Note: This article is part of the course Introduction to iOS using UIKit I've given many times in the past. The course was originally in Spanish, and I decided to release it in English so more people can read it and hopefully it will help them.
Swift.Error
So far, we've been using optionals and other techniques to represent errors in Swift.
However, Swift has a native, correct way of representing errors, and that way is using custom enum types implementing Swift.Error
, or simply Error
.
The Error
protocol won't ask us to implement any method or property.
Let's suppose we are modeling a banking app, so we could define our errors with a enum like this, where each of its cases will represent a different error, and the enum itself will implement the Error
protocol.
enum AccountError: Error {
case insufficientFunds
case wrongPinCode
case wrongAlphabeticCode
}
throws, throw
Once we've defined our Error
enum, we can implement functions that use those errors. However, in order to do that, we must specify that our function can throw an error. This is done with the throws
keyword.
class Account {
var funds: Double = 0.o
func deposit(amount: Double) {
funds += amount
}
func extract(amount: Double) throws {
// ...
}
}
In this case, the extract
method could throw an error, if we try to extract an amount we don't have in the account.
Let's then check that using a guard
clause. If we find an error, we need to throw
it specifying which error it is.
func extract(amount: Double) throws {
guard funds >= amount else {
throw AccountError.insufficientFunds
}
funds -= amount
}
do/try/catch
Nice! We can throw errors. Our next challenge is to handle them effectively when we get them.
The first we have to notice is that throwing functions need to be handled in a special way to use them.
Let's start with the code and then we'll explain it:
let account = Account()
account.deposit(amount: 100)
do {
try account.extract(amount: 50)
} catch let error {
switch error {
case AccountError.insufficientFunds:
print("Insufficient funds!")
default:
print("Another error.")
}
}
In the previous example we've defined two different regions.
One, inside the do
block, where we can call throwing functions. Whenever we need to call a function that may throw, we use the try
keyword.
The second region is defined inside the catch
block. Inside the catch
block we can define a custom name for our error. This is done with a let
binding, followed by the error name. In this case, its called error
, which is also the default error name, so it's unneeded in this case.
Inside the catch
block, we can receive our error, which will be of type Error
, so we can switch on it and do something different for each error case we've defined, and a default
clause in case we are getting an error we aren't handling specifically.
try, try?, try!
Let's modify a bit our function so it will return an object of type Extraction
, which will represent the extraction we're performing.
struct Extraction {
let id: String
let amount: Double
init(amount: Double) {
self.id = UUID().uuidString // Random ID string
self.amount = amount
}
}
And for our extract
method:
func extract(amount: Double) throws -> Extraction {
guard funds >= amount else {
throw AccountError.insufficientFunds
}
funds -= amount
return Extraction(amount: amount)
}
Let's suppose we are only interested in getting our Extraction
object, and we aren't interested in the specific error we might get. We can get the Extraction
object directy using any of these two try
variations:
-
try?: Returns the throwing function result as an Optional value, which is
nil
in case the function throws. - try!: Returns the throwing function result as a non-optional value, or crashes in case the function throws.
Of course, we must be very cautious when using try!
. try?
instead, is much more widely used.
Let's see the three cases.
try:
let account = Account()
account.deposit(amount: 100)
do {
let extraction = try account.extract(amount: 50)
// `extraction` is non-optional.
} catch let error {
switch error {
case AccountError.insufficientFunds:
print("Insufficient funds")
default:
print("Another error")
}
}
try?
In this case, extraction
is of type Extraction?
, optional, and doesn't require a do-catch
block.
let extraction = try? account.extract(amount: 50)
try!
In this case, extraction
is of type Extraction
, non-optional, and doesn't require a do-catch
block.
let extraction = try? account.extract(amount: 50)
Result
Another way of working with errors is by using the Result
type, included in the standard Swift library.
Remember that an enum could have values associated to each of its cases. Also, an enum can be generic!
Result
is a generic type with two cases with associated values. The Result
type is defined something like this, and has many methods to work with these two possibilities:
enum Result<SuccessType, ErrorType: Error> {
case success(SuccessType)
case failure(ErrorType)
}
This means that Result
is a type that represents a case in which the operation has been successful, and a second case in which the operation has failed, so we will get the Error
for that failure.
Of course, Result
is included in the standard Swift library, so we won't need to define our own Result
type.
Let's rewrite the extract
function so it'll return Result<Extraction, AccountError>
func extract(amount: Double) -> Result<Extraction, AccountError> {
guard funds >= amount else {
return Result<Extraction, AccountError>.failure(AccountError.insufficientFunds)
}
funds -= amount
let extraction = Extraction(amount: amount)
return Result<Extraction, AccountError>.success(extraction)
}
As it's obvious that the result is of type Result<Extraction, AccountError>
, so we can avoid writing that type in the return
:
func extract(amount: Double) -> Result<Extraction, AccountError> {
guard funds >= amount else {
return .failure(AccountError.insufficientFunds)
}
funds -= amount
let extraction = Extraction(amount: amount)
return .success(extraction)
}
Once our function is rewritten this way, we can modify the code that calls the extract
function.
let extractionResult = account.extract(amount: 50)
switch extractionResult {
case .success(let extraction):
print("Extraction id: \(extraction.id)")
case .failure(let error):
switch error {
case .insufficientFunds:
print("Insufficient funds")
case .wrongAlphabeticCode:
print("Wrong alphabetic code")
case .wrongPinCode:
print("Wrong pin code")
}
}
Top comments (0)