Error Handling in Swift: Try, Catch, and Throw with Code Examples

Husnain Ali
6 min readJul 11, 2024

--

Using try-catch in Swift along with throwing errors allows for graceful handling of failures in your code. A method can be defined as throwing, which means it can throw an error if something goes wrong. To catch this error, a do-catch statement is implemented.

However, using a do-catch statement with a throwing method is not always necessary. Let’s explore all the scenarios and discuss them in detail.

Defining a throwing method with the throws keyword

Creating a throwing method is as easy as adding the throws keyword to a method just before the return statement. In this example, we use a method to update the name for a user of a specific user identifier.

func updateUser(with name: String) {
// This method is not throwing any errors
}

func updateUser(with name: String) throws {
// The throws keyword makes that this method can throw an error
}

When calling a throwing method you might get the following error:

Call can throw but is not marked with ‘try’

Call can throw but is not marked with ‘try’

This means that you have to use the try keyword before a piece of code that can throw an error.

try updateUser(with: "name")

Typed throws are new since Xcode 16 and allow you to define the type of error a method throws. Instead of handling any error, you can handle exact cases and benefit from compiling time checks for newly added instances. They were introduced and designed in Swift Evolution proposal SE-413.

func updateUser(with name: String) throws(ErrorType) {
// The throws keyword makes that this method can throw an error of type ErrorType
}

Handle initializer with throw

A great feature in Swift is the ability to create a throwing initializer, which is useful for validating properties (like a age) before initializing an object, and it can throw an error during execution using the throws keyword.

struct User {
let name: String
let age: Int

enum ValidationError: Error {
case invalidAge
}

init(name: String, age: Int) throws {
guard age > 0 else {
throw ValidationError.invalidAge
}
self.name = name
self.age = age
}
}

let user = try User(name: "User name", age: 30)

Handling Errors with a Do-Catch Statement

To catch a thrown error in Swift we need to use the do-catch statement. The following example uses the earlier defined User instance.

do {
let user = try User(name: "User name", age: 30)
print("User created: \(person)")
} catch {
print("Failed to create user: \(error)")
}

The User struct includes a throwing initializer that validates if the age is greater than 0. If the age is invalid, it throws an ValidationError.invalidAge error, triggering the catch block. In the catch block, you can use a local error property to print the caught error. The catch block is executed only when an error occurs.

Catching a specific or generic type of error

In the following example, we have implemented the name update method. This method can now throw both a user validation error and a database error thrown from the fetchUser method.

func fetchUser() throws -> User {
// Fetches the user from the database
// Can also throw any error from backend
}

func updateUser (with name: String) throws {
guard !name.isEmpty else {
throw ValidationError.emptyName
}
var user = try fetchUser()
user.update(name)
user.save()
}

You can now used typed throws to get better feedback on exactly what error is caught. For example:

enum ValidationError: Error {
case invalidAge, emptyName, nameToShort
}

func updateUser(with name: String) throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError.emptyName
}
// Save user object
}

func updateUser(with age: Int) throws(ValidationError) {
guard age > 0 else {
throw ValidationError.invalidAge
}
// Save user object
}

do {
let user = try updateUser(string: "Hello world!")
} catch {
//the error here is of type "ValidationError" instead of just "Error"
}

For more on Swift this year, be sure to check out What’s new in Swift, and for more on migrating your project to Swift 6, check out Migrate your app to Swift 6

It would be nice to catch the errors in separated blocks to display a different alert if only the name is invalid. There are several ways of doing this:

do {
try updateUser(with: "User name")
} catch InitializationError.emptyName {
// Called only when the `ValidationError.emptyName` error is thrown
} catch InitializationError.nameToShort(let nameLength) where nameLength == 1 {
// Only when the `nameToShort` error is thrown for an input of 1 character
} catch is InitializationError {
// All `ValidationError` types except for the earlier catch `emptyName` error.
} catch {
// All other errors
}

There are a few things to point out here:

  • Order of Catching: The order of catch blocks is important. In this example, we first catch emptyName specifically, followed by other ValidationError cases. If we swapped these two, the specific emptyName catch would never be called.
  • Using where: The where keyword can filter error values. In this example, we specifically catch name inputs with a length of 1 character. If you’re not familiar with the where keyword, you can check out my blog post on its usage in Swift.
  • Using is: The is keyword allows us to catch errors of a specific type.
  • Generic Catch Block: The final generic catch block catches all other errors.

There’s some scenarios in which you’d like to catch two or more specific error types. In this case, you can use lists in your catch statements:


do {
try updateUser(with: "User name")
} catch ValidationError.emptyName, ValidationError.nameToShort {
// Only called for `emptyName` and `nameToShort`
} catch {
// All other errors like age validation error
}

Using try? with a throwing method

In Swift, you can use try? with a throwing method to convert the result into an optional value. If the method throws an error, try? returns nil instead of propagating the error. This is useful when you want to handle errors gracefully without using a do-catch block.
Here’s an example to illustrate how to use try? with a throwing method:

struct User {
enum ValidationError: Error {
case emptyName
}

let name: String

init(name: String) throws {
guard !name.isEmpty else {
throw ValidationError.emptyName
}
self.name = name
}
}

func createUser(with name: String) -> User? {
return try? User(name: name)
}

if let user = createUser(with: "JohnDoe") {
print("User created: \(user.name)")
} else {
print("Failed to create user")
}

if let user = createUser(with: "") {
print("User created: \(user.name)")
} else {
print("Failed to create user")
}

the first call to createUser(with name:) succeeds, while the second call fails due to an invalid name, demonstrating the use of try? to handle throwing methods gracefully.

Using try! with a throwing method

If you want your app to fail instantly you can use try! with the throwing method. This will basically fail your app just like a fatal error statement.

let user = try! createUser(with: "")
print(user.name)

This will end up with the following error as the name or age input is empty or less then equal to 0:

Fatal error: ‘try!’ expression unexpectedly raised an error: ValidationError.emptyName || ValidationError.invalidAge

Conclusion

Error handling in Swift is robust, enabling you to write clean, readable code while managing error scenarios effectively. With the ability to catch specific error types and utilize the where keyword, you can tailor your error handling to address particular situations as needed. Remember to design your methods to throw errors whenever there is a possibility of failure!

If you’d like to connect and improve your Swift knowledge, you can find me on LinkedIn here. For any additional tips or feedback, feel free to reach out to me at husnainali593@gmail.com. Also, take a look at my portfolio at https://ihusnainalii.github.io/.

Thanks!

--

--

Husnain Ali

A Lead iOS Engineer at ILI.DIGITAL AG. Follow me for a dose of practical swift knowledge! Let's explore the iOS ecosystem together. 🚀💻