1. What strict concurrency means
In Swift, strict concurrency is a compile-time safety model for concurrent code. It helps detect places where mutable state could be accessed from multiple execution contexts in an unsafe way. Rather than waiting for random crashes, corrupted values, or impossible-to-reproduce bugs, the compiler flags suspicious code early.
This model becomes especially important as applications grow more asynchronous. Modern Swift programs perform UI work, network requests, database access, caching, and background processing at the same time. Without clear rules for who owns data and where that data can be touched, race conditions become likely.
2. Why it matters
Concurrency bugs are among the hardest problems in software engineering because they often do not fail consistently. A race condition may only appear on a certain device, under load, or after a tiny timing difference. Swift’s design tries to move those problems from runtime to compile time.
Without strict concurrency
- Shared mutable state is easy to access incorrectly.
- Threading assumptions are hidden in comments or habits.
- Crashes and data corruption may appear unpredictably.
With strict concurrency
- Isolation rules are expressed in code.
- Unsafe sharing is surfaced during compilation.
- Architecture becomes easier to reason about.
3. Core building blocks
Actors
An actor protects its mutable state by allowing only one logical executor to
access that state at a time. If a value belongs to an actor, outside code typically must
cross an asynchronous boundary to interact with it.
actor BankAccount {
private var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
func currentBalance() -> Double {
balance
}
}
Sendable
Sendable represents values that can be safely transferred across concurrency
boundaries. Immutable value types often fit naturally. Mutable reference types usually need
more thought.
struct UserProfile: Sendable {
let id: Int
let username: String
}
@MainActor
UI code frequently belongs on the main actor. Marking a type or function with
@MainActor makes that isolation explicit and easier for the compiler to enforce.
@MainActor
final class ProfileViewModel: ObservableObject {
@Published var name = ""
func updateName(to newValue: String) {
name = newValue
}
}
async / await
These keywords make suspension points visible. If code must hop to another actor or wait for an asynchronous operation, Swift wants that pause to be obvious in the source code.
4. What the compiler checks
Strict concurrency is not one single rule. It is a group of rules that work together to reduce unsafe access patterns.
Isolation checking
Prevents actor-isolated state from being accessed from the wrong context.
Sendability checking
Verifies whether values can safely cross task and actor boundaries.
Main actor enforcement
Helps keep UI-related code on the correct executor.
Explicit suspension points
Requires await when execution may suspend or switch isolation.
5. Practical examples
Unsafe shared class
This pattern is common in older code: a mutable class is passed around freely and updated from multiple places.
final class Counter {
var value = 0
}
let counter = Counter()
Task {
counter.value += 1
}
Task {
counter.value += 1
}
The problem is not that the code looks complicated. The problem is that nothing in the type
system explains who owns value or whether concurrent mutation is safe.
Safer actor-based version
actor Counter {
private var value = 0
func increment() {
value += 1
}
func snapshot() -> Int {
value
}
}
let counter = Counter()
Task {
await counter.increment()
}
Task {
await counter.increment()
}
UI update from async work
@MainActor
final class ArticlesViewModel: ObservableObject {
@Published private(set) var title = "Loading..."
func load() {
Task {
let fetchedTitle = await fetchTitle()
title = fetchedTitle
}
}
private func fetchTitle() async -> String {
"Strict Concurrency in Swift"
}
}
Here, the view model is isolated to the main actor, which makes its UI-facing state easier to reason about.
6. Migrating existing code
Adopting strict concurrency is usually a gradual architecture cleanup, not a single switch flip. The best migrations tend to start by identifying where mutable shared state lives and deciding which of these strategies makes sense:
- Convert shared mutable state into an actor.
- Use immutable value types where possible.
- Mark UI-facing code with
@MainActor. - Add
Sendableconformance to safe data models. - Reduce reliance on ad hoc thread-hopping patterns.
A practical migration mindset
- Start with leaf modules or smaller targets.
- Fix warnings that reveal real shared-state ambiguity.
- Move business logic into actors or immutable models.
- Keep UI state clearly isolated to the main actor.
- Only use unsafe escape hatches when you fully understand the tradeoff.
7. Common mistakes
Assuming async means “background thread”
Asynchronous code is about suspension and scheduling, not automatically about a specific thread. Isolation rules matter more than thread guesses.
Overusing shared reference types
Mutable classes that are freely passed between tasks often become the main source of strict concurrency issues.
Ignoring actor boundaries
If a value belongs to an actor, external code should not behave as though it were regular shared memory.
Treating warnings as noise
Many warnings are actually design feedback. They often reveal code whose ownership model is unclear or incomplete.