Handle Errors Using Throwing Functions in Swift

Learn how to handle errors in Swift with `do-catch` blocks and write and call throwing functions.

Introduction

A key question when writing programs is how to handle something going wrong. Optional types provide one excellent strategy for handling these cases. However, optionals cannot distinguish between different types of problems. Take the following example of a Product structure:

struct Product {
let name: String
let price: Double?
}

A Product has two properties: a name and a price. Sometimes, the price might be nil because it hasn’t been entered yet. A store wants to know the average price of a Product and the following function provides that answer:

func averagePrice(of products: [Product]) -> Double? {
guard !products.isEmpty else { return nil }
var sum = 0.0
for product in products {
guard let price = product.price else { return nil }
sum += price
}
return sum / Double(products.count)
}

The averagePrice(of:) method returns nil when either the array of products is empty or at least one of the prices is nil. Once the function returns, there’s no way of telling which of those was the case without going back to the original products array.

Throwing functions give us a way to write functions that can be more descriptive about what went wrong. Let’s take a look at their syntax.

Defining a throwing function

Throwing functions are marked with the throws keyword after the function name and before the return arrow ->:

func averagePrice(of products: [Product]) throws -> Double {
// ...
}

Throwing Errors

Functions that have been marked as throws can throw an Error in their implementation. Error is a Swift protocol with no requirements. Because of this, any type can be marked as conforming to Error. Typically, you should use an enumeration to represent all of the possible errors:

enum AverageCalculationError: Error {
case noProducts
case productWithNoPrice
}

In the enumeration above, the errors that can happen when calling averagePrice(of:) are listed explicitly. The function can be rewritten as a throwing function:

func averagePrice(of products: [Product]) throws -> Double {
guard !products.isEmpty else {
throw AverageCalculationError.noProducts
}
var sum = 0.0
for product in products {
guard let price = product.price else {
throw AverageCalculationError.productWithNoPrice
}
sum += price
}
return sum / Double(products.count)
}

It’s immediately clear what the error is. Additionally, the function no longer returns an optional Double. Either it calculates the average price or it throws an error that specifies what the problem was. Note that because throwing an error stops the execution of the rest of the function, throw can be used in the else block of a guard statement.

The try keyword

Calling with try!

Calling a throwing function could result in an error being thrown. While that gives great information about what went wrong, we need to tell the compiler what to do with the error. Because the call could fail, Swift requires that you use the keyword try before invoking a throwing function.

Using try! means that if an error is thrown, the whole program should crash:

let clothingStore = [
Product(name: "Coat", price: nil),
Product(name: "Shoes", price: nil),
Product(name: "Hat", price: nil)
]
let averageClothingPrice = try! averagePrice(of: clothingStore)
// Fatal error: 'try!' expression unexpectedly raised an error: AverageCalculationError.productWithNoPrice:

The do-catch block

Using try! is like force unwrapping an optional, and should be used only when you are sure that no errors will be thrown. A better way to handle errors is using a do-catch statement. A do-catch statement has the following syntax:

do {
// Call throwing functions
}
catch {
// Handle errors
}

Inside of the do block, you can use the try keyword (no punctuation) to call throwing functions. Any errors that occur will be handled in the catch block.

let clothingStore = [
Product(name: "Coat", price: nil),
Product(name: "Shoes", price: nil),
Product(name: "Hat", price: nil)
]
do {
let averageClothingPrice = try averagePrice(of: clothingStore)
print("The average price is \(averageClothingPrice)")
}
catch {
print("An error occurred: \(error)")
}
// Prints: An error occurred: productWithNoPrice

Because the averagePrice(of:) method throws an error, it is handled in the catch block. Note that there is a variable named error that you have access to in the catch block. This is the error thrown by the function.

This structure also allows for explicit handling of different errors:

let emptyStore = [Product]()
do {
let average = try averagePrice(of: emptyStore)
print("The average price is \(average)")
}
catch AverageCalculationError.noProducts {
print("Error: There are no products in the store")
}
catch AverageCalculationError.productWithNoPrice {
print("Error: There is at least one product without a price")
}
// Prints: Error: There are no products in the store

Calling with try?

If you’d like to call a throwing function safely but don’t need to handle the errors independently, you can use try?. Using try? means that if the call fails, it simply becomes nil.

let officeStore = [
Product(name: "Pen", price: 0.50),
Product(name: "Stapler", price: 5.00),
Product(name: "Paperclips", price: 2.50)
]
if let average = try? averagePrice(of: officeStore) {
print("The average price is \(average)")
}

If the individual errors from a throwing function aren’t important, try? is a good way to safely handle errors.

Review

Throwing functions are a great tool for adding more information about errors. By using do-catch blocks, and the try, throw, and throws keywords, you can now write and call your own throwing functions. As you work with more Swift APIs, you’ll see more methods that throw errors and you are now equipped for handling them.

Author

Codecademy Team

'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'

Meet the full team