Error Handling with Try and Catch in Swift

Written by LearnAppMaking on March 26 2021 in App Development, Swift

Error Handling with Try and Catch in Swift

You use do, try, catch and throw to handle errors and exceptions in Swift. Error handling gives you greater control over unexpected scenarios that might occur in your code, like a user that inputs a wrong account password.

In this tutorial, we’ll discuss:

  • Why catching (and throwing) errors is important
  • Syntax for handling errors in Swift, with do try catch
  • How to throw and rethrow errors and exceptions
  • How to create your own Error types (and why)
  • When to convert errors to optional values with try?
  • Working with try! to disable errors (and its risks)

Ready? Let’s go.

  1. Why Catch Errors and Exceptions?
  2. How to Throw Errors in Swift
  3. How To Handle Errors with Do-Try-Catch
  4. Convert Errors to Optionals with “try?”
  5. Disable Error Handling with “try!”
  6. Further Reading

Why Catch Errors and Exceptions?

Some errors and bugs in your app are the result of a mistake the programmer makes. For example, when your app crashes with Index out of bounds, you probably made typo somewhere. When you force unwrap an optional that’s nil, your app inevitably crashes.

Ideally, your app has 0 of these bugs. Your job as a coder is to find and fix them before your app is published in the App Store. In this tutorial, we’re going to talk about a different kind of error.

In practical iOS development, not all errors are bad. Some errors are part of an app’s lifecycle, such as an “Incorrect password” exception when you accidentally try to log in with the wrong account credentials.

Errors like these are recoverable. You’ll want to catch the error and respond appropriately, like showing the user an alert dialog that informs them about the error.

A few examples:

  • An ATM displays “Incorrect PIN code” when you try to withdraw money
  • Your car shows a “Fuel low” indicator light as you try to start the engine
  • An authentication attempt for an API returns “Wrong username/password”

You can recover from these errors by displaying an alert message, or doing something else. Your credit card may get blocked after 3 failed attempts, for example. Your car can point you to the nearest fuel pump station when you’re out of gas. And you can try to log in with a different username and password.

It’s worth noting here that these errors do not result in a crash that quits the app. It would suck if an ATM machine would need to be restarted if you entered the wrong PIN code! This distinction is important; between recoverable errors and actual bugs that need to get fixed.

As you’ll soon learn, you handle errors in Swift with the do, try, catch and throw keywords, as well as Swift’s native Error type. Swift has first-class support for handling errors, which means that its implemented at a language-level and that it has the same kind of control as if, guard and return, for example.

Let’s move on!

How to Throw Errors in Swift

Before we discuss handling errors, let’s first take a look at throwing them. That happens with the throw keyword in Swift. Every error or exception ‘starts’ when its thrown.

Check this out:

if fuel < 999 {
    throw RocketError.insufficientFuel
}

In the above code, the throw keyword is used to throw an error of type RocketError.insufficientFuel when the fuel variable is less than 999.

In other words, we’re throwing that error when, while running the code, it so happens that there’s not enough fuel. Throwing an error lets you indicate that something unexpected happened.

When you wrote this code in your app, you anticipated that this exception could happen, which is why you put that conditional and throw code in there.

Imagine we’re trying to launch a rocket. Like this:

func igniteRockets(fuel: Int, astronauts: Int) throws
{
    if fuel < 999 {
        throw RocketError.insufficientFuel
    }
    else if astronauts < 3 {
        throw RocketError.insufficientAstronauts(needed: 3)
    }

    // Ignite rockets
    print("3... 2... 1... IGNITION!!! LIFTOFF!!!")
}

The function igniteRockets(fuel:astronauts:) will only ignite the rockets if fuel is greater or equal to 999 and if there are at least 3 astronauts on board.

In the above code we’re using an error type called RocketError. Here’s what it looks like:

enum RocketError: Error {
    case insufficientFuel
    case insufficientAstronauts(needed: Int)
    case unknownError
}

This enumeration extends the Error protocol. It defines three types of errors: .insufficientFuel, insufficientAstronauts(needed) and .unknownError. You can only use throw with a value that’s represented by the Error type. Defining your own error types is super useful, because you can be clear about what these errors mean in your code.

Take for instance the insufficientAstronauts(needed:) error. When this error is thrown (see above code) you can provide an argument that indicates how many astronauts are needed to successfully ignite the rocket. Neat!

The throw keyword has the same effect as the return keyword. When throw is invoked, execution of the function stops at that point and the thrown error is passed to the caller of the function. At that point it needs to get handled with a do try catch block, which we’ll discuss in the next section.

The big question is, of course, could you implement the same functionality without making use of throw? For example, like this:

func igniteRockets(fuel: Int, astronauts: Int)
{
    if fuel >= 999 && astronauts >= 3 {
        print("3... 2... 1... IGNITION!!! LIFTOFF!!!")    
    } else {
        // ... do what!?
    }
}

Notice the difference with the previous function? This is an architectural question worth thinking about.

Error handling with throw, try and catch is a tool that can help you make your code easier to read, maintain and extend. You have a few alternatives at your disposal, so – as with anything in coding – the devil is in the details. What tool serves you best?

One last thing – the igniteRockets(···) function is marked with the throws keyword. It indicates to whoever calls this function that errors must be handled. Swift forces us to handle errors (or rethrow them), which means you can’t accidentally forget it! It’s a feature that makes Swift safer to use, with fewer errors, because you catch them sooner.

Speaking of handling errors, let’s discuss that next!

How To Handle Errors with Do-Try-Catch

In Swift, you handle errors with a do-try-catch block, to take appropriate action when an error is thrown.

Here’s an example:

do {
    try igniteRockets(fuel: 5000, astronauts: 1)    
} catch {
    print(error)
}

Handling errors with do try catch has 3 important aspects:

  1. Prepend the function (or expression) that can throw with the try keyword
  2. Wrap the code that has try in a do { ··· } block
  3. Attach one or more catch { ··· } blocks to handle all or individual errors

In many other programming languages, like Java or C++, this approach to handle errors is called try/catch (without do). The expression that can throw errors isn’t explicitly marked with try. Swift makes this explicit.

Looking at the above code, you can imagine what happens next. The igniteRockets() function throws an error, and the catch block is subsequently invoked. This prints out the error with print(error).

It’s good to know that, even though no constant with the name error has been declared in the catch block, this value is available implicitly. Within a catch block, you can use the error constant to get the error that’s thrown.

You can also declare the error value explicitly, like this:

do {
    ···
} catch(let exception) {
    print(exception.localizedDescription)
}

With do-try-catch, you can also respond to error cases individually. Remember the RocketError enum we defined earlier? Now check this out:

do {
    try igniteRockets(fuel: 5000, astronauts: 1)    
} catch RocketError.insufficientFuel {
    print("The rocket needs more fuel to take off!")
} catch RocketError.insufficientAstronauts(let needed) {
    print("You need at least \(needed) astronauts...")
}

The above catch blocks are invoked based on enumeration cases of RocketError. Its syntax is similar to that of a switch block. You can directly access associated values of enum cases, such as the needed constant in the above example.

You can also use expressions and pattern matching with where to get more control over the catch block that’s triggered for an error.

Why don’t you give it a try yourself?

enum RocketError: Error {
case insufficientFuel
case insufficientAstronauts(needed: Int)
case unknownError
}

func igniteRockets(fuel: Int, astronauts: Int) throws
{
if fuel < 999 {
throw RocketError.insufficientFuel
}
else if astronauts < 3 {
throw RocketError.insufficientAstronauts(needed: 3)
}

// Ignite rockets
print("3... 2... 1... IGNITION!!! LIFTOFF!!!")
}

do {
try igniteRockets(fuel: 5000, astronauts: 1)
} catch {
print(error)
}

Quick Tip: You can provide a textual representation of an error by creating an extension for your custom error type, conforming to the LocalizedError protocol. Use the extension to implement the errorDescription, failureReason, helpAnchor and recoverySuggestion properties. Iterate over the error enum, and return string values that describe the error.

Convert Errors to Optionals with “try?”

The purpose of error handling is to explicitly determine what happens when an error occurs. This allows you to recover from errors, instead of just letting the app crash.

In some cases, you don’t care much about the error itself. You just want to receive value from a function, for example. And if an error occurs, you’re OK with getting nil returned.

You can do this with the try? keyword. It’ll convert any error that occurs into an optional value. The syntax combines try with a question mark ?, in a similar fashion as working with optionals. When you use try?, you don’t have to use the complete do-try-catch block.

Here’s an example:

let result = try? calculateValue(for: 42)

Imagine the calculateValue(for:) function can throw errors, for example if its parameter is invalid. Instead of handling this error with do-try-catch, we’re converting the returned value to an optional.

One of two things will now happen:

  1. Doesn’t throw an error: The function returns a value, that’s assigned to result
  2. Throws an error: No value is returned, so result is nil

Handling errors this way means you can benefit from syntax specific to optionals, such as the nil-coalescing operator ?? and optional binding. Like this:

if let result = try? calculateValue(for: 99) {
    // Do something with non-optional value "result"
}

Using nil-coalescing operator ?? to provide a default value:

let result = try? calculateValue(for: 123) ?? 9000

Be careful with using try? too often. It’s compelling to use try? to ignore or silence errors, so don’t get into the habit of using try? to avoid having to deal with unexpected errors.

Error handling with do-try-catch is a feature for a reason, because it generally makes your code safer and less prone to errors. Avoiding to recover from errors only leads to (quiet) bugs later on.

What if you’re coding a function that includes a call to another function that can throw errors, but you don’t want to deal with the error right there? Mark your own function with rethrows. Like its name implies, this will “re-throw” the error to the caller of your function.

Disable Error Handling with “try!”

You can also disable error handling entirely, with try!. This effectively stops the propagation of the error – and crashes your app.

Just as the try? keyword, the try! syntax combines try with an exclamation mark !, in a similar fashion to force unwrapping optionals. When you use try!, you don’t have to use the complete do-try-catch block.

Unlike try?, which returns an optional value, the try! syntax will crash your code if an error occurs. There are 2 distinct scenarios in which this is useful:

  1. Use try! when your code could impossibly result in an error, i.e. when it’s 100% certain – from the context of your code – that an error cannot occur
  2. Use try! when you can’t recover from an error, and it’s impossible to continue execution beyond that point

A few examples:

  • Imagine you’re building an app that includes an image asset that’s shipped with the app. If you’re loading that image from disk, you’re using a function that’ll throw an error if the image file doesn’t exist. Since you’re 100% certain that the image is shipped with the app, you can safely use try! to load the image – because that missing file error is never thrown.
  • Imagine you’re coding an app that has a database file embedded in it. The function to load the database throws an error when the database is corrupted. You can safely use try! to load that database into memory, because when the database is corrupted, the app won’t be usable anyway.

Further Reading

Handling errors with do-try-catch helps you to write more robust and fault-tolerant code. You can use it to recover from errors, for example by displaying an error message to the user.

With syntax like try? and try! you can write more concise code. And custom error types give you the opportunity to communicate what kind of error has occurred, and respond accordingly. Neat!

A few tutorials pair exceptionally well with this one. They are:

Want to learn more? Check out these resources:

LearnAppMaking

LearnAppMaking

At LearnAppMaking.com, app developers learn how to build and launch awesome iOS apps.