Swift Async/Await

Swift async/await is a new programming model introduced in Swift 5.5 (released on September 20, 2021) for writing asynchronous code that is easy to read, write, and understand. It provides a more natural and intuitive way of writing asynchronous code than traditional completion handlers.

Async/await is a programming model that has been adopted by several modern programming languages years before Swift: ECMAScript 2017, C# 5.0 in 2012, Python 3.5 in 2015, Kotlin 1.3 in 2018.

Asynchronous programming

In traditional synchronous programming, a task is executed sequentially, one after another. If a task takes a long time to complete, it blocks the execution of other tasks and makes the application unresponsive.

Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be able to be responsive to other events while that task runs, rather than having to wait until that task has finished. Once that task has finished, your program is presented with the result.

Asynchronous programming is useful for tasks that take a long time to complete, such as downloading a file from the internet or making an HTTP request. It can also be used for tasks that are CPU-bound, such as performing a complex calculation.

In iOS development, asynchronous programming can be achieved using various mechanisms, including:

  • Grand Central Dispatch (GCD): A low-level API provided by Apple that enables the execution of tasks concurrently. It allows developers to specify the quality of service and the dispatch queue on which a task should be executed.
  • Operation Queues: A higher-level API for executing tasks concurrently. They allow developers to define operations as objects that can be added to a queue, and executed concurrently or serially.
  • Completion Handlers: Blocks of code that are executed after a task completes. They are commonly used to implement asynchronous networking operations.

These methods can be difficult to use and error-prone.

Meet new async/await

This is a new language feature that allows us to write asynchronous code in a synchronous looking way. It enables us to write asynchronous code that behaves like synchronous code, with no callbacks or completion handlers required.

With async/await, we can define functions that are asynchronous by using the async keyword in the function signature. Within an asynchronous function, we can use the await keyword to suspend execution until the awaited task completes.

Here’s a sample code that uses async/await to fetch data from a server:

enum NetworkError: Error {
    case invalidURL
    case requestFailed
    case invalidData
}

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

func fetchUsers() async throws -> [User] {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
        throw NetworkError.invalidURL
    }

    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw NetworkError.requestFailed
    }

    do {
        let decoder = JSONDecoder()
        let users = try decoder.decode([User].self, from: data)
        return users
    } catch {
        throw NetworkError.invalidData
    }
}

To call this function from another async function or method, you would use the await keyword to wait for the result, like this:

func doSomethingElse() async {
  do {
      let users = try await fetchUsers()
      print(users)
  } catch {
      print(error)
  }
}

To call the fetchUsers function from a synchronous context, you can use Task.init or Task.detached:

Task {
    do {
        let users = try await fetchUsers()
        print(users)
    } catch {
        print(error)
    }
}

Benefits of async/await

Swift’s new async/await feature is a powerful mechanism for writing asynchronous code in a much more readable and maintainable way compared to the above methods such as Grand Central Dispatch, Operation Queues, and Completion Handlers. Here are some reasons why async/await is considered better:

Simplifies asynchronous code: With async/await, developers can write asynchronous code that looks and feels like synchronous code. It allows developers to write asynchronous code in a more linear and readable way, without having to deal with complex callback chains, dispatch queues, and operations.

Easy error handling: With async/await, error handling is simpler and more natural. Developers can use the try/catch syntax to handle errors, which makes error handling more consistent with the rest of the Swift language.

Safer concurrency: async/await provides safer concurrency by eliminating the risk of common concurrency issues such as race conditions and deadlocks. Since async/await enforces a linear execution model, the chances of two concurrent tasks modifying the same resource at the same time are greatly reduced.

Improved performance: With async/await, developers can write more efficient code by avoiding the overhead of creating and managing dispatch queues and operations.

Better integration with Swift’s other language features: async/await is integrated with Swift’s existing language features such as closures, function types, and generics, making it easier to write generic and reusable asynchronous code.

Drawbacks of async/await

While async/await offers many benefits, there are also some potential drawbacks to using this programming model:

Compatibility with older versions of Swift: async/await was introduced in Swift 5.5, so if you need to support older versions of Swift, you won’t be able to use it. You’ll need to use other techniques such as completion handlers or dispatch queues to perform asynchronous operations.

Learning curve: async/await introduces new concepts and programming patterns that may take some time to learn and become comfortable with. Developers who are used to traditional synchronous programming may need to adjust their thinking and adopt new approaches to writing asynchronous code.

Debugging: debugging asynchronous code can be more challenging than synchronous code, and async/await is no exception. Issues like race conditions and deadlocks can be more difficult to diagnose and fix in async/await code.

Performance: while async/await can improve the performance of some types of asynchronous code, it can also introduce additional overhead in some cases. For example, using async/await to perform a simple synchronous operation can actually be slower than doing it synchronously.

API compatibility: not all APIs are designed to work with async/await. APIs that rely heavily on completion handlers or delegates may not be easily adapted to async/await, and may require additional workarounds or adaptations.