UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Error handling in Swift: try, catch, do and throw

Paul Hudson    September 23rd 2019    @twostraws

Swift works hard to make sure we can write safe software, which means it eliminates many opportunities for our code to fail. One of the ways it accomplishes this is by letting us catch errors when risky code doesn't run according to plan, and in this article I'm going to walk through how that works and how to use it in your own code.

The Swift approach: try, catch, do and throw

If we wanted to load a file from a URL into a `Data` instance, we might write code like this:

let contents: Data?
do {
    contents = try Data(contentsOf: someURL)
} catch {
    contents = nil
}

That illustrates three of the five new keywords you'll need to learn.

The fourth and fifth keywords are throw and throws, and we'll look at them in depth now.

Please create a new Xcode project, using the Single View App template. You can name it whatever you feel like, and target whatever device you want – it doesn't matter, because we're not doing anything visual here.

Select ViewController.swift and add this new method:

func encrypt(_ str: String, withPassword password: String) -> String {
    // complicated encryption goes here
    let encrypted = password + str + password
    return String(encrypted.reversed())
}

That method is going to encrypt an string using the password that gets sent in. Well, it's not actually going to do that – this article isn't about encryption, so my "encryption" algorithm is pathetic: it puts the password before and after the input string, then reverses it. You're welcome to add the complex encryption algorithm yourself later on!

Modify viewDidLoad() to call that method by adding this:

let encrypted = encrypt("secret information!", withPassword: "12345")
print(encrypted)

When you run your app now, you'll see "54321!noitamrofni terces54321" printed out in the Xcode terminal. Easy, right?

But there's a problem: assuming you actually do put in a meaningful encryption algorithm, there's nothing stopping users from entering an empty string for a password, entering obvious passwords such as "password", or even trying to call the encryption method without any data to encrypt!

Swift comes to the rescue: you can tell Swift that this method can throw an error if it finds itself in an unacceptable state, such as if the password is six or fewer characters. Those errors are defined by you, and Swift goes some way to ensuring you catch them all.

To get started, we need the throws keyword, which you add to your method definition before its return value, like this:

func encrypt(_ str: String, withPassword password: String) throws -> String {
    // complicated encryption goes here
    let encrypted = password + str + password
    return String(encrypted.reversed())
}

As soon as you do that, your code stops working: adding throws has actually made things worse! But it's worse for a good reason: Swift's try/catch system is designed to be clear to developers, which means you need to mark any methods that can throw using the try keyword, like this:

let encrypted = try encrypt("secret information!", withPassword: "12345")

…but even now your code won't compile, because you haven't told Swift what to do when an error is thrown. This is where the do and catch keywords come in: they start a block of code that might fail, and handle those failures. In our basic example, it might look like this:

do {
    let encrypted = try encrypt("secret information!", withPassword: "12345")
    print(encrypted)
} catch {
    print("Something went wrong!")
}

That silences all the errors, and your code runs again. But it's not actually doing anything interesting yet, because even though we say encrypt() has the potential to throw an error, it never actually does.

How to throw an error in Swift

Before you can throw an error, you need to make a list of all the possible errors you want to throw. In our case, we're going to stop people from providing empty passwords, short passwords and obvious passwords, but you can extend it later.

To do this, we need to create an enum that represents our type of error. This needs to build on the built-in Error enum, but otherwise it's easy. Add this before class ViewController:

enum EncryptionError: Error {
    case empty
    case short
}

That defines our first two encryption error types, and we can start using them immediately. As these are preconditions to running the method, we're going to use the new guard keyword to make our intentions clear.

Put this at the start of encrypt():

guard password.count > 0 else { throw EncryptionError.empty }
guard password.count >= 5 else { throw EncryptionError.short }

If you run the app now nothing will have changed, because we're providing the password "12345". But if you set that to an empty string, you'll see "Something went wrong!" printed in the Xcode console, showing the error.

Of course, having a single error message isn't helpful – there are several ways the method call can fail, and we want to provide something meaningful for each of them. So, modify the try/catch block in viewDidLoad() to this:

do {
    let encrypted = try encrypt("secret information!", withPassword: "")
    print(encrypted)
} catch EncryptionError.empty {
    print("You must provide a password.")
} catch EncryptionError.short {
    print("Passwords must be at least five characters, preferably eight or more.")
} catch {
    print("Something went wrong!")
}

Now there are meaningful error messages, so our code is starting to look better. But you may notice that we still need a third catch block in there even though we already caught both the .empty and .ehort cases.

Swift wants exhaustive try/catch error handling

If you recall, I said "Swift goes some way to ensuring you catch them all" and here's where that becomes clear: we're catching both errors we defined, but Swift also wants us to define a generic catch all to handle any other errors that might occur. We don't tell Swift what kind of error our encryption method might throw, just that it throws something, so this extra catch-all block is required.

This does have one downside: if you add any future values to the enum, which we're about to do, it will just drop into the default catch block – you won't be asked to provide any code for it as would happen with a switch/case block.

We're going to add a new value to our enum now, to detect obvious passwords. But we're going to use Swift's super-powerful enums so that we can return a message along with the error type. So, modify the EncryptionError enum to this:

enum EncryptionError: Error {
    case empty
    case short
    case obvious(String)
}

Now when you want to throw an error of type EncryptionError.obvious you must provide a reason.

guard password != "12345" else { throw EncryptionError.obvious("I've got the same passcode on my luggage!") }

Obviously you don't want to provide hundreds (or thousands!) of guard statements to filter out obvious passwords, but hopefully you remember how to use UITextChecker to do spell checking – that would be a smart thing here!

That's our basic do/try/throw/catch Swift example complete. You might look at the try statement and think it useless, but it's primarily there to signal to developers "this call might fail." This matters: when a try calls fails, execution immediately jumps to the catch blocks, so if you see try before a call it signals that the code beneath it might not get called.

There's one more thing to discuss, which is what to do if you know a call simply can't fail, for whatever reason. Now, clearly this is a decision you need to make on a case-by-case basic, but if you know there's absolutely no way a method call might fail, or if it did fail then your code was so fundamentally broken that you might as well crash, you can use try! to signal this to Swift.

When you use the try! keyword, you don't need to have do and catch around your code, because you're promising it won't ever fail. Instead, you can just write this:

let encrypted = try! encrypt("secret information!", withPassword: "12345")
print(encrypted)

Using the try! keyword communicates your intent clearly: you're aware there's the theoretical possibility of the call failing, but you're certain it won't happen in your use case. For example, if you're trying to load the contents of a file in your app's bundle, any failure effectively means your app bundle is damaged or unavailable, so you should terminate.

That's all for error handling in Swift. If you'd like to learn about how Swift handles try/finally you should read my article on Swift's defer keyword.

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.