Swift Concurrency: The Complete Guide

Published on Β· Updated Β· 21 min

Wlad
Wlad
Founder & Swift Tech Lead

Swift Concurrency revolutionized asynchronous programming on Apple platforms. Introduced with Swift 5.5 in 2021, the model has evolved significantly up to Swift 6.2 which finally simplifies its usage with "Approachable Concurrency". This guide covers everything you need to know, from beginner to expert.

Why Swift Concurrency?

Before Swift 5.5, asynchronous programming relied on callbacks, completion closures, and Grand Central Dispatch (GCD). This model worked but created problems:

  • Pyramid of doom with nested callbacks

  • Fragmented error handling that's easy to forget

  • No protection against data races

  • Code that's hard to read and maintain

Swift Concurrency solves these problems with a model built into the language. The compiler verifies concurrent access safety and async code looks like synchronous code.

Fundamentals: async/await

The async keyword marks a function that can suspend its execution. The await keyword indicates a potential suspension point.

A crucial point: await never blocks the thread. It frees the thread for other tasks while waiting. This is fundamentally different from a sleep or synchronous wait.

Suspension and Continuations

When the runtime encounters an await, it splits your function into "partial tasks". Each segment between two await calls is an execution unit that the runtime can schedule independently.

The runtime can interleave these partial tasks with those of other async functions, optimizing system resource usage.

Task: The Unit of Asynchronous Work

Task is the fundamental container for asynchronous work. It provides an execution context, manages priorities, and propagates cancellation.

Task Types

Swift offers several Task types depending on your needs:

The difference between Task and Task.detached is important. A standard Task inherits the caller's context (actor, priority, task-local values). A Task.detached starts in a clean context.

Task Priorities

Swift defines several priority levels that influence scheduling:

The runtime can also perform "priority escalation": if a high-priority task awaits a low-priority task, the latter is temporarily promoted.

async let: Simple Parallelism

When you have multiple independent operations, async let executes them in parallel:

Note the difference with sequential execution:

async let creates child tasks that are automatically cancelled if an error occurs or the scope ends.

Cooperative Cancellation

Swift uses a cooperative cancellation model. When you cancel a Task, it's not killed abruptly. The runtime marks it as cancelled, and it's up to your code to check and react.

Two methods to check for cancellation:

withTaskCancellationHandler

For long-running operations with callbacks, use withTaskCancellationHandler:

Async Error Handling

async throws functions propagate errors naturally:

With async let, if a parallel task fails, the others are automatically cancelled:

SwiftUI Integration

SwiftUI integrates naturally with Swift Concurrency via the .task modifier:

The .task(id:) modifier automatically restarts when the identifier changes:

Swift 6.2: Approachable Concurrency

Swift 6.2 (Xcode 26) introduces major changes to simplify concurrency. The "Improving the approachability of data-race safety" document acknowledges that the model had become too complex.

MainActor by Default (SE-0466)

The most impactful change: you can configure an entire module to run on MainActor by default.

With this setting, all code without explicit annotation runs on MainActor:

In Xcode 26, new projects have this option enabled by default.

Async on Caller's Actor (SE-0461)

Before Swift 6.2, nonisolated async functions automatically jumped to a background thread:

With Swift 6.2, async functions stay on the caller's actor:

This behavior is enabled via the upcoming feature NonisolatedNonsendingByDefault.

@concurrent: Explicit Opt-in

If you want a function to truly execute in parallel, Swift 6.2 introduces @concurrent:

This explicit distinction makes code much more readable.

Isolated Conformances (SE-0470)

SE-0470 allows restricting a protocol conformance to a specific actor:

Without @MainActor Equatable, the compiler could execute == on any thread, violating the class isolation.

Best Practices

1. Prefer Structured Concurrency

Use async let and TaskGroup rather than Task.detached when possible. Structured concurrency ensures child tasks are automatically cleaned up.

2. Check Cancellation Regularly

In long loops, periodically check for cancellation:

3. Avoid Strong Captures in Tasks

Use [weak self] to avoid retain cycles:

4. Use the Right Isolation Level

Don't put everything on MainActor. Reserve it for UI code:

Common Pitfalls

1. Forgetting await

The compiler warns you, but be careful in contexts where await is implicit:

2. Blocking the MainActor

Avoid long operations on MainActor:

3. Task.sleep vs Synchronous Sleep

Always use Task.sleep for delays:

Migrating from GCD

If you have existing GCD code, here are the equivalences:

GCDSwift Concurrency

DispatchQueue.main.async

Task { @MainActor in }

DispatchQueue.global().async

Task.detached { }

DispatchGroup

TaskGroup

DispatchSemaphore

AsyncStream or Actors

DispatchWorkItem.cancel()

task.cancel()

Migration example:

Going Further

Swift Concurrency is a vast topic. Here are the recommended next steps:

  • TaskGroup and ThrowingTaskGroup for dynamic parallelism

  • Actors for shared data isolation

  • AsyncStream for asynchronous value streams

  • Sendable and data isolation

Official Resources