Async/Await: A Modern Approach to Concurrency

Concurrency in Swift has evolved significantly over the years. The introduction of the async/await paradigm marks a shift from older threading concepts like Grand Central Dispatch (GCD) and Operation Queue. To fully grasp the benefits of async/await, it's essential to understand why this new paradigm is necessary and how it simplifies concurrent programming.

The Need for Async/Await

Many developers have used URLSession for network requests. However, from the syntax alone, it's often unclear whether URLSession executes on a different thread. Determining if a method runs on the main thread or a background thread requires a deep understanding of the framework. This ambiguity can lead to potential threading issues and bugs. To address these limitations, Apple introduced the async/await framework, providing a clearer and more concise way to handle asynchronous operations.

URLSession and Concurrency


Consider the example of URLSession.shared.dataTask. Without prior knowledge of the framework, it's not evident whether this method executes asynchronously. This uncertainty necessitates a more explicit and straightforward approach, which async/await provides.



Async Functions


Asynchronous functions in Swift are similar to regular functions but with the added ability to suspend execution partway through. They can pause mid-execution while waiting for some operation to complete. Within the body of an asynchronous function or method, each suspension point is indicated with the `await` keyword. To declare a function or method as asynchronous, you use the `async` keyword.


func fetchData() async -> String { }


An async function can also throw errors. When an async function might throw an error, you need to use the `throws` keyword after `async`.

func fetchData() async throws -> String { }



The Await Keyword


The `await` keyword is used to call an async method. When the system encounters the `await` keyword, it pauses execution, and the next line only runs once the async function has completed.

For example, the `Task.sleep` method, which takes time intervals in nanoseconds, pauses code execution. When calling the `sleep` method, you need to use the `await` keyword.

func fetchData() async throws -> String {
    print("\(#function) execution started \(Date())")
    try await Task.sleep(nanoseconds: 5 * 1_000_000_000) // 5 seconds
    print("\(#function) execution finished \(Date())")
    return "Hello, World"
}

// Output
// fetchData() execution started 2024-05-23 15:05:00 +0000
// fetchData() execution finished 2024-05-23 15:05:05 +0000


In the above code, `fetchData() execution finished` will be printed 5 seconds after `fetchData() execution started`.

## Task

A `Task` in Swift allows you to create and manage units of work that can run concurrently. Here’s a simple example:

```swift
Task {
    do {
        print("Task 1")
        let result = try await fetchData()
        print(result)
    } catch {
        print("Error: \(error)")
    }
}

Task {
    do {
        print("Task 2")
        let result = try await fetchData()
        print(result)
    } catch {
        print("Error: \(error)")
    }
}

// Output
// Task 1
// Task 2
// fetchData() execution started 2024-05-23 15:18:21 +0000
// fetchData() execution started 2024-05-23 15:18:21 +0000
// fetchData() execution finished 2024-05-23 15:18:26 +0000
// fetchData() execution finished 2024-05-23 15:18:26 +0000
// Hello, World
// Hello, World
```

An async function must be called from within a `Task` block. If you have multiple `Task` blocks, they will execute in parallel.

## Async/Await vs. Do/Try/Catch

The syntax for async/await is similar to the do/try/catch syntax used for handling errors. Any function that could throw an error must be marked with the `throws` keyword. Similarly, any function that could pause execution must be marked with the `async` keyword.

```swift
func fetchWithThrows() throws { }

func fetchWithAsync() async { }
```

Any function that includes `throws` in its signature must be called using the `try` keyword within a `do-catch` block. Similarly, any function that includes `async` in its signature must be called using the `await` keyword within a `Task` block.

```swift
do {
    try fetchWithThrows()
} catch {
    print("Error: \(error)")
}

Task {
    await fetchWithAsync()
}
```

Any function that throws an error must be called within a `do-catch` block. If it is not, the calling function must also be marked with the `throws` keyword. Similarly, when calling any async function, it must be called within a `Task` block. If it is not, the calling function must be marked with the `async` keyword.

```swift
func callingThrows() throws {
    try fetchWithThrows()
}

func callingAsync() async {
    await fetchWithAsync()
}
```

Ultimately, `throws` need to be handled within a `do-catch` block, and `async` needs to be handled within a `Task` block.

## Conclusion

The introduction of async/await in Swift provides a modern and intuitive approach to concurrency. By clearly indicating suspension points with the `await` keyword and managing asynchronous functions with the `async` keyword, developers can write more readable and maintainable code. Embracing this new paradigm allows for a more straightforward and error-free way of handling concurrent operations, making Swift development even more robust and efficient.