Race condition in one sentence

A race condition occurs when a program’s result depends on which concurrent operation finishes first.

It is not the same as a data race. A data race is unsynchronized access to the same memory where at least one operation writes. A race condition can happen even with thread-safe code when the logical order of operations is wrong.

Swift’s concurrency checks help prevent data races, but they cannot determine whether one asynchronous operation logically depends on another.

Sequential execution

Use sequential execution when operations must complete in a specific order.

try await loadConfiguration()
try await loadCommands()

loadCommands() starts only after loadConfiguration() finishes successfully.

Parallel execution with async let

Use async let when the number of independent operations is known in advance.

async let profileTask = loadProfile()
async let assetsTask = loadAssets()

let (profile, assets) = try await (
    profileTask,
    assetsTask
)

Both child tasks start concurrently. The Swift runtime decides whether they execute physically in parallel.

Parallel execution with TaskGroup

Use a task group to start multiple independent tasks and wait until all of them finish.

await withTaskGroup(of: Void.self) { group in
    group.addTask { await refreshProfile() }
    group.addTask { await refreshAssets() }
    group.addTask { await refreshNotifications() }

    await group.waitForAll()
}

A task group is especially useful when tasks are added dynamically or when several operations share the same result type.

Quick choice

  • Use consecutive await calls when execution order matters.
  • Use async let for a fixed number of independent operations.
  • Use TaskGroup for multiple or dynamically created independent operations.
  • Do not run dependent operations concurrently.

The key rule is simple: operations should run concurrently only when they are truly independent.