As modern apps become more reliant on network requests, file operations, and other time-consuming tasks, handling asynchronous code efficiently has never been more crucial. Swift’s introduction of async/await marks a significant milestone, offering developers a cleaner and more intuitive way to write asynchronous code. In this post, we’ll delve into how async/await works in Swift and how you can leverage it to write more readable and maintainable code. Async await has become as part of the new structured concurrency in Swift 5.5. WWDC 2021.

The Challenge of Asynchronous Code

Before async/await, Swift developers often used closures, completion handlers, delegates, or third-party libraries like Combine to handle asynchronous operations. While these tools are powerful, they can lead to complex code structures, often referred to as ‘callback hell’, making the codebase difficult to read and maintain.

Consider the following example using completion handlers:

func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) {
    // Perform network request
}

fetchUserData { result in
    switch result {
    case .success(let user):
        // Process user data
    case .failure(let error):
        // Handle error
    }
}

While this works, nesting multiple asynchronous calls can quickly become unwieldy.

Introducing Async/Await

Async/await simplifies asynchronous programming by allowing you to write asynchronous code that looks and behaves like synchronous code. Here’s how it works:

  • async Functions: Functions that perform asynchronous tasks are marked with the async keyword.
  • await Keyword: Used to call an async function and wait for its result.

Using async/await, the previous example becomes:

func fetchUserData() async throws -> User {
    // Perform network request
}

Task {
    do {
        let user = try await fetchUserData()
        // Process user data
    } catch {
        // Handle error
    }
}

Notice how the code is flatter and more readable. The asynchronous call looks like a regular function call, and error handling integrates seamlessly with Swift’s try-catch mechanism.

How Async/Await Works

To define an asynchronous function, add the async keyword before the return type:

func performTask() async {
    // Asynchronous code
}

If the function can throw errors, include the throws keyword:

func performTask() async throws {
    // Asynchronous code that may throw an error
}

You can call an async function using the await keyword within an async context:

await performTask()

If you’re not already in an async function, you can use a Task to create an asynchronous context:

Task {
    await performTask()
}

Error handling with async functions uses the standard do-catch blocks:

do {
    try await performTask()
} catch {
    // Handle error
}
Practical Examples

Here’s how you might fetch data from a web API using async/await:

func fetchData(from url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

Task {
    do {
        let url = URL(string: "https://api.example.com/data")!
        let data = try await fetchData(from: url)
        // Use the fetched data
    } catch {
        // Handle error
    }
}

Async/await allows you to run multiple asynchronous tasks in parallel:

async let firstResult = fetchFirstData()
async let secondResult = fetchSecondData()

do {
    let (firstData, secondData) = try await (firstResult, secondResult)
    // Use both results
} catch {
    // Handle error
}

In this example, fetchFirstData() and fetchSecondData() run concurrently, and you await both results before proceeding.

Async Sequences

Swift’s async/await also introduces AsyncSequence and AsyncIterator for handling sequences of asynchronous data, such as streams.

Reading Lines from a File:

func readLines(from url: URL) async throws {
    let handle = try FileHandle(forReadingFrom: url)
    for try await line in handle.bytes.lines {
        print(line)
    }
}
Actors for Data Isolation

Swift introduces actors to safely manage mutable state in a concurrent environment. Actors ensure that their state is accessed in a thread-safe manner.

Defining an Actor:

actor DataManager {
    private var dataStore: [String: Any] = [:]

    func updateData(forKey key: String, value: Any) {
        dataStore[key] = value
    }

    func fetchData(forKey key: String) -> Any? {
        return dataStore[key]
    }
}

Using the Actor:

let manager = DataManager()

Task {
    await manager.updateData(forKey: "username", value: "swiftUser")
    let username = await manager.fetchData(forKey: "username")
    print(username ?? "No username found")
}
Conclusion

Async/await brings a much-needed simplification to asynchronous programming in Swift. By allowing developers to write code that is both readable and efficient, it paves the way for building more robust and responsive applications. Whether you’re fetching data from a network, reading files, or performing any time-consuming operations, async/await is a powerful tool to have in your Swift toolkit.

If you have any questions or feedback, feel free to reach out to me on or