Advanced Concurrency

Consuming AsyncSequence with for-await

July 15, 2026
5 min read
Featured image for blog post: Consuming AsyncSequence with for-await

You know how to await a single value. But what about a stream of values that arrive over time?

Follow along with the code: iOS-Practice on GitHub

The for-await Loop

AsyncSequence is like Sequence, but each element is delivered asynchronously. You consume it with for await:

for await number in numberStream {
    print("Got: \(number)")
}
print("Stream finished")

The loop suspends at each iteration, waiting for the next value. When the sequence ends, the loop exits normally.

How It Works

struct NumberStream: AsyncSequence {
    typealias Element = Int

    let count: Int
    let delay: TimeInterval

    struct AsyncIterator: AsyncIteratorProtocol {
        var current = 0
        let count: Int
        let delay: TimeInterval

        mutating func next() async throws -> Int? {
            guard current < count else { return nil }

            try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
            current += 1
            return current
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(count: count, delay: delay)
    }
}

The key method is next():

  • Returns the next value when available
  • Returns nil to signal the end
  • Can throw errors

Cancellation is Automatic

One of the best features: task cancellation works automatically.

let task = Task {
    for await value in someStream {
        process(value)
    }
}

// Later...
task.cancel()  // Loop exits cleanly

When the task is cancelled, the for await loop stops iterating. No manual checking required.

Breaking Early

You can exit a for await loop early just like a regular loop:

for await message in chatMessages {
    if message.contains("goodbye") {
        break  // Stop consuming the stream
    }
    display(message)
}

Error Handling

For throwing sequences, wrap in do-catch:

do {
    for try await data in networkStream {
        process(data)
    }
} catch {
    print("Stream failed: \(error)")
}

The Mental Model

Think of for await as:

  1. Pull-based: You request the next value
  2. Suspending: Waits without blocking a thread
  3. Cancellation-aware: Respects task cancellation
  4. Sequential: One value at a time, in order

Interview Tip

When discussing AsyncSequence, mention that it's the async equivalent of Sequence—same protocol pattern (makeAsyncIterator, next()), but with suspension points. This shows you understand the design philosophy, not just the syntax.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch