Debugging Applications With Xcode

Swift Error and Exception Breakpoints

Debugging Applications With Xcode
Resources

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 useful to debug problems. Remember that the Cocoa frameworks are for the most part written in C and Objective-C.

Swift Error Breakpoint

An Example

Let's start with the Swift error breakpoint. Open Xcode, create a new project based on the Single View App template, and name it Breakpoints.

Creating a New Project

Creating a New Project

Open ViewController.swift and define a private throwing method named loadJSONData(). The loadJSONData() method loads JSON data from a file in the application 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 application bundle for the URL of a file named 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 application bundle.

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

We pass the URL instance to one of the initializers of the Data struct. We use another guard statement and the try? keyword to create the Data instance. If we're unable to create a Data instance, we throw 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 instance to a JSON object. We return the result if the operation succeeds, otherwise we throw an 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 the viewDidLoad() method of the ViewController class. Because loadJSONData() is a throwing method, we wrap it in a do-catch statement. We print any errors that are thrown to Xcode's console.

override func viewDidLoad() {
    super.viewDidLoad()

    do {
        // Load JSON Data
        let json = try loadJSONData()

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

Run the application to test the implementation of the loadJSONData() method. You may already know what's about to happen. Because we haven't added a file named data.json to the application bundle, the loadJSONData() method throws an error and the error is printed to the console.

An error is thrown.

Printing errors to the console is a common habit among developers. While there's nothing wrong with printing errors to the console, the result is often that errors are missed, overlooked, or ignored. 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 application one more time to see the result. The debugger pauses the application 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 application.

It's important to remember that the debugger only pauses the execution of the application if an error is thrown in the code you've written. Let me illustrate this. Add a new file to the project by choosing the Empty template, name the file data.json, and 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 application. Unsurprisingly, the application is paused by the debugger because the loadJSONData() method throws an error.

Another error is thrown.

This example illustrates that the debugger only pauses the application if an error is thrown in the code you've written. The debugger pauses the application on line 44 of ViewController.swift. Even though the JSONSerialization class throws an error on line 43 when the conversion of the Data instance to a JSON object fails, that error doesn't result in the interruption of the application.

Propagating Errors

A powerful feature of error handling in Swift is the ability to propagate errors. Take a look at this example.

import UIKit

class ViewController: UIViewController {

    enum JSONError: Error {

        case resourceNotFound
        case unableToLoadData
        case invalidJSON

    }

    override func viewDidLoad() {
        super.viewDidLoad()

        do {
            // Load JSON Data
            let json = try loadJSONData()

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

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

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

        return json
    }

    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
    }

}

I slightly modified the previous example by defining another throwing method, loadData(). The loadData() method is in charge of loading the contents of the file. The loadJSONData() method invokes the loadData() method and uses the result to convert the Data instance to a JSON object.

Because the loadJSONData() method is throwing, there's no need to wrap the loadData() method invocation in a do-catch statement. Any errors that are thrown in the loadJSONData() method are automatically propagated to the call site of the method.

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

Another error is thrown.

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

Error Handling and Objective-C

Even though error handling isn't built into Objective-C, the Foundation framework defines the NSError class to fill that gap. Error handling in Swift takes advantage of the NSError class and neatly interoperates with Objective-C. Any Objective-C method that accepts an NSError pointer as its last parameter becomes a throwing method in Swift.

Exception Breakpoint

The exception breakpoint works in a similar fashion. Comment out the do-catch statement and define a struct named User at the top of ViewController.swift.

import UIKit

struct User {

    let first: String
    let last: String

}

class ViewController: UIViewController {

    ...

}

Let's trigger an exception by creating a User instance and storing it in the user defaults database. We create an instance in the viewDidLoad() method of the ViewController class and store it in the user defaults database.

override func viewDidLoad() {
    super.viewDidLoad()

    // Initialize User
    let currentUser = User(first: "Bart", last: "Jacobs")

    // Store User in User Defaults
    UserDefaults.standard.set(currentUser, forKey: "currentUser")
}

If we run the application, it's almost immediately terminated. It seems as if the debugger has paused the application in the AppDelegate class. Notice, however, that the pointer is red, not green, and that it reads signal SIGABRT. This indicates that the process of the application was terminated due to a fatal error. The stack trace isn't very helpful to debug the issue.

The application is terminated.

The output in Xcode's console is more helpful. It shows us that an NSInvalidArgumentExcpetion was thrown by Core Foundation. That's what caused the application to be terminated. Remember that you can only store property list objects in the user defaults database.

2018-03-19 09:22:45.492803+0100 Breakpoints[30563:4022641] [User Defaults] Attempt to set a non-property-list object Breakpoints.User(first: "Bart", last: "Jacobs") as an NSUserDefaults/CFPreferences value for key currentUser
2018-03-19 09:22:45.500432+0100 Breakpoints[30563:4022641] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object Breakpoints.User(first: "Bart", last: "Jacobs") for key currentUser'

...

libc++abi.dylib: terminating with uncaught exception of type NSException

Let's see how an exception breakpoint can help us debug issues like this. Open the Breakpoint Navigator, click the + button in the lower left, and choose Exception Breakpoint. Xcode offers us several options. We can choose which exceptions we want the debugger to break on, Objective-C exceptions, C++ exceptions, or both.

Adding an exception breakpoint.

We can also tell the debugger when it should pause the application, when the exception is thrown or when it is caught.

Adding an exception breakpoint.

Set Exception to Objective-C and leave Break on On Throw.

Adding an exception breakpoint.

Run the application again to see what happens. This time the application isn't terminated. The debugger pauses the application in ViewController.swift. Because we try to store a non-property list object in the user defaults database, Core Foundation throws an exception. The stack trace is also more helpful, taking us to the root of the problem.

The application breaks on an exception being thrown.

Setting an exception breakpoint won't change the outcome of the code we wrote, but it gives us more context to debug the issue. Without the exception breakpoint set, the application crashes. We can put the pieces together by inspecting the output in Xcode's console.

With the exception breakpoint set, however, the debugger pauses the application the moment an exception is thrown. This means that we can inspect the application when things hit the fan. The debugger breaks the application on the offending line in the viewDidLoad() method. That gives us more information and options to debug the issue.

Even though the application is written entirely in Swift, the Foundation and Core Foundation frameworks are written in C and Objective-C. When something goes haywire, an exception is thrown.

Swift error and exception breakpoints are very useful. In most of my projects, the exception breakpoint is always enabled because it makes debugging issues related to incorrect use of the frameworks easier.

The Swift error breakpoint enables you to quickly find out where an error is thrown, taking you to the location of the problem. It saves you time and adds clarity when debugging. Remember that the debugger only pauses the application when your application throws an error. If one of Apple's frameworks throws an error, the Swift error breakpoint isn't hit.

What's Next

In the next episode, we zoom in on the test failure and the constraint error breakpoints. These breakpoint types may not seem very useful at first glance, but I can assure you that they come in handy if used correctly.

Resources
Next Episode "Test Failure and Constraint Error Breakpoints"