Sitemap

Swift Concurrency Explained: async/await, AsyncSequence, and Parallel Tasks

4 min readOct 27, 2026

--

Modern iOS applications frequently perform multiple operations: fetching data from APIs, loading images, listening to live updates, and processing background tasks.

Before Swift Concurrency, developers often used completion handlers, which could make complex asynchronous workflows difficult to read and maintain.

Swift introduced async/await, AsyncSequence, and structured concurrency to make asynchronous code easier to understand and manage.

Let’s explore three important concepts with practical Swift examples.

1. Defining and Calling Asynchronous Functions

An asynchronous function is a function that can suspend its execution while waiting for an operation to complete, allowing other work to make progress.

Swift uses two important keywords:

  • async — declares a function that can suspend.
  • await — marks a call where execution may suspend while waiting for a result.

Example: Fetching a user profile

func fetchUserName() async -> String {
return "Abhishek"
}

func displayUser() async {
let name = await fetchUserName()
print("Welcome, \(name)!")
}

What’s happening here?

  1. async indicates that fetchUserName() is asynchronous.
  2. await marks the point where the caller may suspend until the result is available.
  3. Once the result returns, execution continues with the next statement.

For operations that can fail, combine async with throws:

func fetchUser() async throws -> String {
// Perform a network request.
return "User fetched successfully"
}

func loadUser() async {
do {
let user = try await fetchUser()
print(user)
} catch {
print("Failed to fetch user:", error)
}
}

Notice the order: try await.

Important: async does not automatically make a function run on a background thread. It allows the function to suspend, and Swift manages the execution of asynchronous tasks.

When should you use it?

Use async/await when working with network requests, database operations, file access, or any API that supports asynchronous execution.

It also helps replace deeply nested completion handlers with straightforward, readable code.

2. Asynchronous Sequences (AsyncSequence)

A regular Sequence provides elements that you iterate over using for-in.

But what if the next element isn’t available yet?

For example, imagine receiving live messages, monitoring a stream of events, or processing values as they arrive over time.

This is where AsyncSequence becomes useful.

Get Abhishek Jadhav’s stories in your inbox

Join Medium for free to get updates from this writer.

Instead of waiting for an entire collection to become available, you can process each element asynchronously.

Example: Consuming an asynchronous stream

func observeUpdates() async {
let updates = AsyncStream<Int> { continuation in
continuation.yield(10)
continuation.yield(20)
continuation.yield(30)
continuation.finish()
}

for await update in updates {
print("Received:", update)
}
}

Output:

Received: 10
Received: 20
Received: 30

How does it work?

  • AsyncStream provides an asynchronous sequence of values.
  • yield(_:) publishes an element to the stream.
  • finish() signals that no more elements will be produced.
  • for await consumes elements one at a time and can suspend while waiting for the next element.

This example emits values immediately, but the same pattern can support values arriving over time.

When should you use it?

Consider AsyncSequence for:

  • Streaming API responses.
  • Live event updates.
  • File or byte-stream processing.
  • Consuming asynchronous notifications or other event sources.

Unlike an asynchronous function that typically returns one result, an asynchronous sequence lets you consume multiple results over time.

3. Calling Asynchronous Functions in Parallel

Consider an iOS screen that needs to fetch a user profile, their orders, and their notifications.

These three requests are independent. Do they need to wait for one another? Not necessarily.

Let’s compare sequential execution with concurrent execution.

Approach 1: Sequential execution

func loadDashboard() async {
let profile = await fetchProfile()
let orders = await fetchOrders()
let notifications = await fetchNotifications()

print(profile, orders, notifications)
}

Each operation is awaited before the next one starts.

If each request takes approximately one second, the total waiting time could be around three seconds.

Approach 2: Concurrent execution with async let

func loadDashboard() async {
async let profile = fetchProfile()
async let orders = fetchOrders()
async let notifications = fetchNotifications()

let dashboard = await (
profile,
orders,
notifications
)

print(dashboard)
}

Here, async let starts child tasks for the independent operations. They can make progress concurrently rather than waiting for one another to finish.

If all three requests take approximately one second and overlap effectively, the total waiting time could be closer to one second, excluding additional overhead.

Key difference: await waits at each individual call, whereas async let allows independent work to start before its results are needed.

Final Thoughts

These three concepts solve different problems in Swift Concurrency:

  • async/await makes asynchronous function calls easier to read and maintain.
  • AsyncSequence lets you consume values that become available over time.
  • async let let independent operations make progress concurrently.

Understanding when to use each one helps you build responsive iOS applications without unnecessarily complicated asynchronous code.

Remember: concurrency isn’t just about making code faster. It’s about structuring asynchronous work so that it’s readable, efficient, and easier to reason about.

Further Reading

For a deeper explanation and more examples, refer to Apple’s official Swift documentation:

The Swift Programming Language — Concurrency

--

--