DEV Community

Fernando Martín Ortiz
Fernando Martín Ortiz

Posted on • Updated on

Introduction to Swift error handling

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
}
Enter fullscreen mode Exit fullscreen mode

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 {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.")
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

And for our extract method:

func extract(amount: Double) throws -> Extraction {
    guard funds >= amount else {
        throw AccountError.insufficientFunds
    }

    funds -= amount
    return Extraction(amount: amount)
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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")
        }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)