Async / Await with Exception Handling

In my previous articles on structured concurrency with Swift, I walked through some practical examples of how I envision developers using the async / await syntax to streamline and simplify asynchronous code. This article extends the basics of async / await with an existing standard - exception handling. 

While I fully support the idea that syntax comes secondary to solution design, there are times when understanding syntax goes a long way to mastering the process. In Swift, language features like optionals and closures come to mind in addition to exception handling. Let’s review two scenarios of exception handling when combined with structured concurrency. 

Building The Exception Model

To start, let’s build a model that mimics the process of downloading a series of images. While connecting to an external process to retrieve images is reasonably common, it certainly doesn’t mean its success is guaranteed. As such, we can extend our implementation to support a custom ImageError enum to illustrate standard exceptions:

enum ImagesError: Error {
    case timeout
    case invalidFormat
    case invalidSize
    case invalidUrl
}

Like this article? Get more content like this when you sign-up for the weekly iOS Computer Science Lab.

With our Error enum type in place, let’s build out the process to download images. As part of the implementation, notice how the throws keyword comes directly after async in the imageFromService method signature:

class Images {
           
    func imageFromService(url: String?) async throws -> String {
        
        //check for errors
        guard let location = url else {
            throw ImagesError.invalidUrl
        }
        
        if location != "www.waynewbishop.com" {
            throw ImagesError.invalidFormat
        }
                
        for _ in 1...10 {
            print("sleeping for \(location)")
            await Task.sleep(20)
        }        
        return location + "_IMG001"
    }
}

Uncaught Exceptions with Async / Await

With our image retrieval functionality in place, the next step involves creating an Images instance. Since the method we want to try will possibly throw an error, one option is to have complier force an uncaught exception at runtime:

    //marking a unit test as throws will mark the XCTest as fail.
    func testImageThrows() async throws {

        let images = Images()
        
        //uncaught exception will be thrown at runtime..
        let result = try await images.imageFromService(url: "www.foo.com")
        print(result)
    }


Since our specific imageFromService method is marked as async, any calls to the function must also run in an asynchronous context. In this case, we’ve identified our entire unit test (e.g., testImageThrows) as async. 


Once established, we can await the results of imageFromService and can prefix this syntax with a try. Since the unit test is also marked as throws, any uncaught exception is identified at runtime. If you execute the unit test directly from Xcode, the specific test will also be marked with a red X (in contrast to the green checkmark), indicating the test has failed - a nice touch. 


Catching Exceptions with Async/Await 

The alternate technique for executing the code is catching any thrown exceptions rather than having the compiler produce a runtime error. However, since we’ll attempt to catch an exception from within an asynchronous process, we’ll need to add extra plumbing to make this work:

 func testImageThrowsCatch() {

        //caught is detected at compile time..
        let images = Images()
                
        // create expectation for a background download task.
        let expectation = XCTestExpectation(description: "images download")
                
        async {
            do {
                
                defer {
                    print("do complete..")
                }
                
                let result = try await images.imageFromService(url: "www.foo.com")
                print(result)
                
            } catch is ImagesError { //generic error
                XCTFail("unable to contact external service") //does this work?
                
            } catch {
                // else
            }
            
            expectation.fulfill()
            
        } //end async
        
        // timeout of 3 seconds.
        wait(for: [expectation], timeout: 3.0)        
    
    } //end function

Even though we call the same imageFromService method, we’ve implemented a different technique of executing our test method (testImageThrowsCatch) as a normal synchronous process. In this example, the call imageFromService is wrapped inside an async{} task-closure. 

To catch the thrown exception, our try keyword must also be wrapped within a do{} statement. As part of the implementation, note how we must also use XCTestExpectation. Without this additional mechanism, the call to XCTFail (to register the test failure) will go unnoticed as this UI-based interaction occurs on the main thread.