Skip to content
All essays
iOSMarch 27, 202514 min

Swift Concurrency Patterns: Advanced async/await

Master advanced Swift concurrency patterns. Actors, tasks, streams, and synchronization.

Ü
Ümit Uz
Mobile & Full Stack Developer

Advanced concurrency patterns for building responsive, efficient Swift applications with async/await.

Task Management

Task Groups

swift
func fetchAllUsers() async throws -> [User] {
  try await withThrowingTaskGroup(of: User.self) { group in
    let ids = ["1", "2", "3", "4", "5"]

    for id in ids {
      group.addTask {
        try await self.fetchUser(id: id)
      }
    }

    var users: [User] = []
    for try await user in group {
      users.append(user)
    }

    return users
  }
}

func fetchWithLimit() async throws -> [User] {
  try await withThrowingTaskGroup(of: User.self) { group in
    let ids = Array(1...100).map { String($0) }

    for id in ids {
      group.addTask {
        try await self.fetchUser(id: id)
      }
    }

    // Process as they complete
    var users: [User] = []
    for try await user in group {
      users.append(user)

      if users.count >= 10 {
        group.cancelAll()
        break
      }
    }

    return users
  }
}

Task Priorities

swift
func fetchWithPriority() async {
  // High priority task
  let highPriorityTask = Task(priority: .high) {
    await fetchCriticalData()
  }

  // Background task
  let backgroundTask = Task(priority: .background) {
    await fetchAnalytics()
  }

  // User-initiated task
  let userTask = Task(priority: .userInitiated) {
    await fetchUserData()
  }

  await highPriorityTask.value
  await userTask.value
  await backgroundTask.value
}

func checkCancellation() async {
  let task = Task {
    for i in 0...100 {
      try Task.checkCancellation()
      print("\(i)")
      try? await Task.sleep(nanoseconds: 100_000_000)
    }
  }

  // Cancel after 1 second
  Task {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    task.cancel()
  }

  await task.value
}

Actor Patterns

Data Isolation

swift
actor DatabaseManager {
  private var cache: [String: Data] = [:]
  private var accessCount: [String: Int] = [:]

  func get(key: String) async -> Data? {
    accessCount[key, default: 0] += 1
    return cache[key]
  }

  func set(key: String, value: Data) async {
    cache[key] = value
  }

  func remove(key: String) async {
    cache.removeValue(forKey: key)
  }

  func getStats() async -> (cacheSize: Int, totalAccess: Int) {
    (cache.count, accessCount.values.reduce(0, +))
  }
}

// Usage
let db = DatabaseManager()

await db.set(key: "user1", value: userData)
let data = await db.get(key: "user1")
let stats = await db.getStats()

Actor Reentrancy

swift
actor SafeAccount {
  private var balance: Double = 0

  func deposit(amount: Double) async {
    balance += amount
  }

  func withdraw(amount: Double) async throws -> Double {
    guard balance >= amount else {
      throw AccountError.insufficientFunds
    }

    // Potential suspension point
    try await Task.sleep(nanoseconds: 100_000_000)

    balance -= amount
    return balance
  }

  func transfer(amount: Double, to other: SafeAccount) async throws {
    try await withdraw(amount: amount)
    await other.deposit(amount: amount)
  }
}

Global Actors

swift
@globalActor
struct DatabaseActor {
  static let shared = DatabaseActorExecutor()
}

actor DatabaseActorExecutor {
  // Main actor executor
}

@DatabaseActor
class UserRepository {
  func fetchUser(id: String) async throws -> User {
    // Automatically runs on DatabaseActor
    try await database.query("SELECT * FROM users WHERE id = \(id)")
  }
}

AsyncStream Patterns

Creating Streams

swift
func createNumberStream() -> AsyncStream<Int> {
  return AsyncStream { continuation in
    Task {
      for i in 1...10 {
        continuation.yield(i)
        try? await Task.sleep(nanoseconds: 500_000_000)
      }
      continuation.finish()
    }
  }
}

func processStream() async {
  let stream = createNumberStream()

  for await number in stream {
    print("\(number)")
  }
}

AsyncThrowingStream

swift
func createErrorStream() -> AsyncThrowingStream<Int, Error> {
  return AsyncThrowingStream { continuation in
    Task {
      for i in 1...10 {
        if i == 5 {
          continuation.finish(throwing: StreamError.invalidValue)
          return
        }
        continuation.yield(i)
      }
      continuation.finish()
    }
  }
}

func processWithError() async throws {
  let stream = createErrorStream()

  do {
    for try await number in stream {
      print("\(number)")
    }
  } catch {
    print("Error: \(error)")
  }
}

Stream Buffering

swift
enum StreamError: Error {
  case overflow
}

func createBufferedStream() -> AsyncStream<Int> {
  return AsyncStream(
    Int.self,
    bufferingPolicy: .bufferingNewest(10)
  ) { continuation in
    // Producer
  }
}

Continuation Patterns

Unsafe Continuation

swift
func fetchUserLegacy(id: String) async throws -> User {
  try await withUnsafeThrowingContinuation { continuation in
    legacyAPI.fetchUser(id: id) { result in
      switch result {
      case .success(let user):
        continuation.resume(returning: user)
      case .failure(let error):
        continuation.resume(throwing: error)
      }
    }
  }
}

func fetchWithTimeout(timeout: TimeInterval) async throws -> User {
  try await withThrowingTaskGroup(of: User.self) { group in
    group.addTask {
      try await fetchUserLegacy(id: "123")
    }

    group.addTask {
      try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
      throw APIError.timeout
    }

    guard let user = try await group.next() else {
      throw APIError.unknown
    }

    group.cancelAll()
    return user
  }
}

Synchronization

Async Lock

swift
actor AsyncLock {
  private var isLocked = false
  private var waiters: [CheckedContinuation<Void, Never>] = []

  func acquire() async {
    if !isLocked {
      isLocked = true
      return
    }

    await withCheckedContinuation { continuation in
      waiters.append(continuation)
    }
  }

  func release() {
    guard let waiter = waiters.first else {
      isLocked = false
      return
    }

    waiters.removeFirst()
    waiter.resume()
  }
}

func useLock() async {
  let lock = AsyncLock()

  await lock.acquire()
  defer { await lock.release() }

  // Critical section
}

Async Barrier

swift
actor AsyncBarrier {
  private var arrived = 0
  private var waiters: [CheckedContinuation<Void, Never>] = []
  private let count: Int

  init(count: Int) {
    self.count = count
  }

  func wait() async {
    arrived += 1

    if arrived == count {
      // Release all waiters
      for waiter in waiters {
        waiter.resume()
      }
      waiters.removeAll()
      arrived = 0
    } else {
      await withCheckedContinuation { continuation in
        waiters.append(continuation)
      }
    }
  }
}

Cancellation

Cooperative Cancellation

swift
func cooperativeTask() async {
  await withTaskCancellationHandler(
    operation: {
      for i in 0...100 {
        try? Task.checkCancellation()
        print("\(i)")
        try? await Task.sleep(nanoseconds: 100_000_000)
      }
    },
    onCancel: {
      print("Task was cancelled")
    }
  )
}

Cancellation Propagation

swift
func parentTask() async {
  let childTask1 = Task {
    await childTask()
  }

  let childTask2 = Task {
    await anotherChildTask()
  }

  // Cancel propagates to children
  childTask1.cancel()

  await childTask1.value
  await childTask2.value
}

Best Practices

  1. 1Structured Concurrency: Use structured tasks
  2. 2Cancellation: Handle cancellation properly
  3. 3Actor Isolation: Protect shared state
  4. 4Error Handling: Handle async errors
  5. 5Testing: Test async code
  6. 6Performance: Profile async code
  7. 7Memory: Manage memory properly
  8. 8Debugging: Debug concurrency issues

Master concurrency for responsive apps!

Related essays

Next essay
TypeScript 5.8 Features: What's New in 2025