Error Handling In Swift With Do-Try-Catch

Written by Reinder de Vries on January 15 2019 in App Development

Error Handling In Swift With Do-Try-Catch

You use error handling with do-try-catch in Swift to respond to recoverable errors. It gives you greater control over different faulty scenarios that might occur in your code, such as a user inputting a wrong username and password.

In this article, we’ll discuss:

  • Why you would “throw” and “catch” errors
  • The syntax you use to handle errors, with do-try-catch
  • How you can create your own custom error types
  • Different useful scenarios for using try? and try!

Ready? Let’s go.

  1. Why Throw And Catch Errors?
  2. Throwing Errors In Swift
  3. Handling Errors With Do-Try-Catch
  4. Converting Errors To Optionals With “try?”
  5. Disabling Error Handling With “try!”
  6. Further Reading

Why Throw And Catch Errors?

In your app, some errors are the result of a mistake a programmer makes. When your app crashes with the “Index out of bounds” message, you probably made a mistake somewhere. And likewise, when you force-unwrap an optional that’s nil, your app crashes.

In practical iOS development, not all errors are bad. Some errors are part of an app’s lifecycle, such as an “Insufficient funds” message when you try to pay with your credit card.

These kinds of errors are recoverable. They can be caught, handled, and responded to appropriately.

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 gets 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 a different username and password.

Swift has first-class support for error handling with the do-try-catch block of code. “First-class” in this case means that do-try-catch has the same kind of control over your app as if and return, for example.

With do-try-catch you can handle errors that have been thrown with throw. We’ll get into this syntax later on. What’s interesting for now, is the throwing and catching principle.

Learn how to build iOS apps

Get started with iOS 12 and Swift 5

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 5 and Xcode 10.

Throwing Errors In Swift

When a scenario that should result in an error occurs in your code, you can throw an error. Like this:

if fuel < 1000 {
    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 1000.

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

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

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

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

The igniteRockets(...) function is also marked with the throws keyword. This keyword indicates to whoever calls this function that errors need to be handled. Swift forces us to handle errors (or rethrow them), which means you can’t accidentally forget it!

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 Error and defines three types of errors: .insufficientFuel, insufficientAstronauts(needed) and .unknownError. Defining your own error types is super useful, because you can be very 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. So. when throw is executed, execution of the function stops at that point and the thrown error is passed to the caller of the function.

Handling Errors With Do-Try-Catch

We can now use error handling to appropriately respond to these error scenarios. Error handling in Swift is done with a so-called do-try-catch block.

Like this:

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

Handling errors with do-try-catch has three important aspects:

  • The function or expression that can produce an error is prepended with the try keyword
  • The block of code that includes the try keyword is wrapped within do { ... }
  • One or more catch { ... } blocks can be attached to the do { ... } block, to handle all or individual error cases

Interestingly, in most other programming languages this error handling mechanism is called try/catch, and the error-producing expression isn’t marked with try. Swift makes this explicit.

You can imagine what happens next. The igniteRockets(...) function produces an error, so subsequently the catch block is invoked. This prints out the error with print(error). By the way, this error variable is implicitly available within the catch block. Nice!

You can also respond to error cases individually. Here’s an example:

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 individual enum cases of RocketError. It’s syntax is similar to that of a switch block. And you can also directly access the associated values of the enum cases, such as needed.

You can also use expressions and pattern matching with where to get greater control over the faulty scenario.

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 < 1000 {
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.

Converting 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 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. The function does not throw an error, returns a value, which is assigned to result
  2. the function throws an error, and does not return a value, which means that nil is assigned to result

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

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

And using nil-coalescing operator ?? to provide a default value:

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

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 potential errors. Error handling with do-try-catch is a feature for a reason, because it generally makes your code safer. Avoiding to recover from errors only leads to bugs later on.

Disabling Error Handling With “try!”

You can also disable error handling entirely, with try!.

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 two distinct scenarios in which this is useful:

  • Use try! when your code could impossibly lead to an error, i.e. when it’s 100% certain that an error will not occur
  • Use try! when you can’t recover from an error, and it’s impossible to continue execution beyond that point

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.

Learn how to build iOS apps

Get started with iOS 12 and Swift 5

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 5 and Xcode 10.

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 concise code. And custom error types give you the opportunity to communicate what kind of error has occurred, and respond accordingly.

Want to learn more? Check out these resources:

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.