Error handling is a key aspect of the Swift language. In several ways errors in Swift are similar to exceptions in Objective-C and C++. Both errors and exceptions indicate that something didn't go as planned. The Swift error and exception breakpoints are useful to debug scenarios in which errors or exceptions are thrown.

Even if your project is entirely written in Swift, adding an exception breakpoint can be helpful when you are debugging an issue. Remember that the Cocoa frameworks are for the most part written in C and Objective-C.

Breaking on Errors

Let's start with the Swift error breakpoint. Open Xcode and create a new project. Choose the App template from the iOS > Application section. Name the project Breakpoints.

Creating a New Project

Creating a New Project

Open ContentView.swift and define a private, throwing method with name loadJSONData(). The loadJSONData() method loads JSON data from a file in the app bundle, converts it to a JSON object, and returns the result.

private func loadJSONData() throws -> Any {

}

Before we implement the loadJSONData() method, we need to define an enum for the errors the method can throw. Define an enum at the top and name it JSONError. The enum conforms to the Error protocol.

enum JSONError: Error {
    
}

The JSONError enum defines three cases, resourceNotFound, unableToLoadData, and invalidJSON.

enum JSONError: Error {
    
    case resourceNotFound
    case unableToLoadData
    case invalidJSON
    
}

In the loadJSONData() method, we first ask the app bundle for the URL of a file with name data.json. We use a guard statement to exit early and throw an error if no file with that name and extension is present in the app bundle.

private func loadJSONData() throws -> Any {
    guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else {
        throw JSONError.resourceNotFound
    }
}

We pass the URL object to the initializer of the Data struct. We use another guard statement and the try? keyword to create the Data object. If the app is unable to create a Data object, it throws another error.

private func loadJSONData() throws -> Any {
    guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else {
        throw JSONError.resourceNotFound
    }
    
    guard let data = try? Data(contentsOf: url) else {
        throw JSONError.unableToLoadData
    }
}

We take advantage of the JSONSerialization class to convert the Data object to a JSON object. The loadJSONData() method returns the result if the operation succeeds, otherwise it throws another error.

private func loadJSONData() throws -> Any {
    guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else {
        throw JSONError.resourceNotFound
    }
    
    guard let data = try? Data(contentsOf: url) else {
        throw JSONError.unableToLoadData
    }
    
    guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else {
        throw JSONError.invalidJSON
    }
    
    return json
}

Let's invoke the loadJSONData() method in a task using the task view modifier. Because loadJSONData() is a throwing method, we wrap the method call in a do-catch statement. We print any errors that are thrown to the console.

var body: some View {
    VStack {
        Image(systemName: "globe")
            .imageScale(.large)
            .foregroundStyle(.tint)
        Text("Hello, world!")
    }
    .padding()
    .task {
        do {
            let json = try loadJSONData()

            print(json)
        } catch {
            print(error)
        }
    }
}

Run the app to test the implementation of the loadJSONData() method. You may already know what's about to happen. Because we didn't add a file with name data.json to the app bundle, the loadJSONData() method throws an error. We can inspect the error in the console.

An error is thrown.

Printing errors to the console is a habit many developers have. While there's nothing wrong with printing errors to the console, the result is often that errors are missed, ignored, or overlooked. The Swift error breakpoint can help you spot errors more easily.

Open the Breakpoint Navigator on the left, click the + button in the lower left, and choose Swift Error Breakpoint.

Adding a Swift error breakpoint.

Run the app one more time to see the result. The debugger suspends the process of the app on the line of the throw statement. This is very convenient if you want to stay on top of the errors that are thrown in your app.

The process is suspended when an error is thrown in your code.

The debugger suspends the process if an error is thrown in your code or the code of a library or framework. Let me illustrate this. Add a new file to the project by choosing the Empty template, name the file data.json, and add a few words to the file. The contents of data.json is invalid JSON.

Add an empty file to the project.

Add invalid JSON to the file.

Run the app. The debugger suspends the process because the jsonObject(with:options:) method of the JSONSerialization class throws an error.

Another error is thrown.

This example illustrates that the debugger suspends the process if an error is thrown in your code or the code of a library or framework.

Suspending the process of your app every time an error is thrown is, most of the time, not what you want. Fortunately, you have the option to define which errors you are interested in. Right-click the Swift error breakpoint and choose Edit Breakpoint... from the contextual menu. We can specify the type of errors we are interested in. Enter JSONError in the Type field.

Edit the Swift error breakpoint.

Run the app one more time. The debugger suspends the process on line 49, not 48. The debugger inspects the value of the Type field and only suspends the process if an error of type JSONError is thrown. This little change makes the Swift error breakpoint much more useful.

The debugger suspends the process if an error of type JSONError is thrown.

Propagating Errors

A powerful feature of error handling in Swift is the ability to propagate errors. Let's update the example to illustrate that concept.

We define another private method with name loadData(). The method is throwing and returns a Data object on success. The loadData() method is responsible for loading the contents of data.json from the app bundle.

private func loadData() throws -> Data {
    guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else {
        throw JSONError.resourceNotFound
    }

    guard let data = try? Data(contentsOf: url) else {
        throw JSONError.unableToLoadData
    }

    return data
}

The loadJSONData() method invokes the loadData() method and uses the result to convert the Data object to a JSON object. Because the loadJSONData() method is throwing, there is no need to wrap the method call in a do-catch statement. Errors thrown by the loadJSONData() method are automatically propagated to the call site.

private func loadJSONData() throws -> Any {
    let data = try loadData()

    guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else {
        throw JSONError.invalidJSON
    }

    return json
}

Remove data.json from the app bundle. What do you think happens if we run the app? Run the app to see if your hypothesis is correct.

Another error is thrown.

The debugger suspends the process in the loadData() method. This example illustrates how helpful the Swift error breakpoint is to find the root cause of a problem. The debugger suspends the process at the location where the error is thrown.

What's Next?

The Swift error breakpoint is quite useful. Most developers are unfamiliar with it or they don't know they can define the type of errors they want to break on. Give it a try the next time you are debugging an issue that involves error handling.