Rich iOS and macOS experiences often perform asynchronous tasks, like getting data from a remote server. New in Swift 5.5, the async and await keywords enable developers to simplify asynchronous code and make it easier to implement asynchronous tasks. This post presents an overview of async and await with an example:

  1. Async
    a. Async Throws
    b. Async Return Value
  2. Await
    a. Await Throws
  3. Async Let
    a. Async Await Let
  4. Async Await Example
    a. Call Async Function From Non Async Code
    b. Async Await vs Closures

Async

In previous versions of Swift, asynchronous programming was often implemented through the use of completion blocks:

// The completion block will be called when
// saveChanges completes its asynchronous logic
func saveChanges(completion: (() -> Void)?) { ... }

// In other code, the saveChanges function would
// be called like this:
saveChanges {
    // Handle completion
}

Using the new async keyword, Swift can mark a function as having asynchronous logic:

// Swift automatically handles asynchronous
// completion of saveChanges
func saveChanges() async { ... }

// In other code, the async saveChanges function
// would be called like this:
await saveChanges()

Async Throws

In previous versions of Swift, some completion blocks returned errors indicating the asynchronous logic in a function could potentially result in an error:

func attachImageToData(
    imageResponse: URLResponse, 
    dataResponse: URLResponse, 
    completion: ((Error?) -> Void)?
)

Using async allows the attachImageToData function to utilize throws, matching the syntax other non-asynchronous functions use for errors:

func attachImageToData(
    imageResponse: URLResponse, 
    dataResponse: URLResponse) async throws

Async Return Value

In previous versions of Swift, some completion blocks contained other types in addition to a potential error. These other types were often logically the return type of asynchronous logic:

func uploadImage(
    image: UIImage, 
    completion: ((URLResponse?, Error?) -> Void)?
)

func uploadData(
    data: Data, 
    completion: ((URLResponse?, Error?) -> Void)?
)

Using async allows the uploadImage and uploadData functions to utilize the expected return type definition -> URLResponse, matching the syntax of other non-asynchronous functions:

func uploadImage(image: UIImage) 
    async throws -> URLResponse
    
func uploadData(data: Data)
    async throws -> URLResponse

Await

The await keyword is used to tell Swift to wait for completion of an asynchronous function:

// Await completion of saveChanges
await saveChanges()

If multiple async functions are called using await, Swift will wait for completion of each function before moving onto the next function:

// Await completion of uploadData 
// before moving to saveChanges
await uploadData(data: Data())

// Await completion of saveChanges 
// before moving forward
await saveChanges()

Await Throws

Like non-asynchronous functions that can throw, try can be used for asynchronous functions in combination with await to call the asynchronous function and throw if an error occurs:

try await attachImageToData(
    imageResponse: imageResponse, 
    dataResponse: dataResponse
)

Async Let

In some cases, async logic requires the return value from an asynchronous function. The async let syntax can be used to mark a let variable as asynchronous, meaning the let variable will be available after some asynchronous logic completes:

async let imageResponse = try uploadImage(
    image: image
)

Async Await Let

Combining multiple async let statements will enable Swift to perform asynchronous logic in parallel:

// Both imageResponse and dataResponse will
// be obtained in parallel
async let imageResponse = try uploadImage(
    image: image
)

async let dataResponse = try uploadData(
    data: data
)

To wait for completion of previous async let variables, use await when the let variable is required to have been initialized:

try await attachImageToData(
    imageResponse: await imageResponse, 
    dataResponse: await dataResponse
)

Alternatively, use await to control concurrency and get only one value at a time:

// By using await, uploadImage 
// will be called first
let imageResponse = try await uploadImage(
    image: image
)

// By using await on uploadImage, 
// uploadData will be called after 
// imageResponse is initialized
let dataResponse = try await uploadData(
    data: data
)

Async Await Example

The async await example presented in this post will reference the following async functions:

func uploadImage(image: UIImage) 
    async -> URLResponse

func uploadData(data: Data) 
     async throws -> URLResponse

func attachImageToData(
    imageResponse: URLResponse, 
    dataResponse: URLResponse) async throws

func saveChanges() async throws

Async await example combining multiple async calls and return values:

// Define an asynchronous function uploadTask that 
// will contain multiple async function calls
func uploadTask(
    image: UIImage, 
    data: Data) async throws {

    // Perform the following async functions 
    // at the same time
    async let imageResponse = try uploadImage(
        image: image
    )

    async let dataResponse = try uploadData(
        data: data
    )
    
    // Wait for completion of attachImageToData 
    // before moving forward
    try await attachImageToData(
        imageResponse: await imageResponse, 
        dataResponse: await dataResponse
    )

    // Wait for completion of saveChanges 
    // before moving forward
    try await saveChanges()
}

Call Async Function From Non Async Code

Initialize a Task to make calls to async functions from non-async code:

func userTappedUpload() {
    Task(priority: .default) {
        do {
            try await self.upload(
                image: UIImage(), 
                data: Data()
            )
        }
        catch {
            // Handle error
        }
    }
}

Async Await vs Closures

One benefit of using async and await is easier to read Swift code. If the async function uploadTask presented in this post is rewritten to only use completion closures, the function has a deep nested callback structure that is hard to read:

func uploadTask(
    image: UIImage, 
    data: Data, 
    completion: ((Error?) -> Void)?) {

    uploadImage(image: image) { imageResponse in
        // Verify imageResponse is successful
        
        self.uploadData(data: data) { 
            dataResponse, dataError in

            // Handle dataError if dataError != nil
            
            self.attachImageToData(
                imageResponse: imageResponse!, 
                dataResponse: dataResponse!) { 
                attachError in

                // Handle attachError if 
                // attachError != nil
                
                self.saveChanges() {
                    completion?(nil)
                }
            }
        }
    }
}

// Example calling upload
upload(image: UIImage(), data: Data()) { error in
    if let error = error {
        // Handle error
    }
}

Asynchronous Programming In Swift

That’s it! By using async and await you can implement asynchronous tasks with callbacks and asynchronous logic in Swift.