The why behind Swift Concurrency
Why Swift Concurrency exists, what problems it solves, and the mental shift behind structure, ownership, and isolation.
The first time I tried to learn Swift Concurrency, I made a huge mistake. And it took me a long time to realize it.
I googled Swift Concurrency, found the first tutorial that seemed very complete, and jumped straight into it.
The tutorial was great. It covered every single concept — async/await, Tasks, actors, @MainActor, and more. I tried to learn how all these APIs worked.
But guess what? It didn’t stick.
I did learn the basics, of course. But every time I needed to do something more complex, I had to go back and relearn things again.
This is what happens when we try to learn the how without stopping to think about the why in the first place.
Some of the basic things I was doing with Swift Concurrency could also be solved with GCD or even Operation Queues. So… why bother? Is it just because Swift Concurrency is a trending topic in the Swift / iOS community right now?
So here’s the question I asked myself — and the one I’ll try to answer in this post:
Why was Swift Concurrency created in the first place?
And why is it better than what we had before?
Swift Concurrency — the why
To answer that, we first need to start with Grand Central Dispatch (GCD).
We’ve all used GCD. I bet you’ve seen something like this before:
DispatchQueue.global().async { [weak self] in
apiClient.loadUser { result in
DispatchQueue.main.async {
guard let self = self else { return }
switch result {
case .success(let user):
self.user = user
case .failure(let error):
self.error = error
}
}
}
}That’s simple, right?
If we want to run some work in the background, we dispatch it to a global queue. If we need to update the UI, we dispatch back to the main queue.
GCD made concurrency approachable — ironic, in hindsight.
Instead of dealing with threads directly, we could just dispatch work to a queue and let the system handle execution. For many years, that was a huge improvement, and for many problems it still works perfectly fine.
💡 Side note: GCD wasn’t always there
Grand Central Dispatch wasn’t part of the original iOS and macOS programming model.
Before GCD, doing work in the background usually meant managing threads directly. Developers had to create threads, coordinate them, and make sure they didn’t step on each other’s toes.
GCD was a big improvement because it let us stop thinking about threads and start thinking about units of work instead. You described what you wanted to run, and the system decided how to schedule it.
That shift made concurrency much more accessible — even if it still left many problems unsolved.
But GCD gives us queues, not structure.
It tells us where code runs, but not how different pieces of work relate to each other.
Once work is dispatched, there’s no built-in concept of ownership. There’s no automatic cancellation. No lifetime guarantees. And no clear relationship between tasks. If you’re not careful, it’s very easy to dispatch work that outlives the feature — or even the screen — that triggered it.
As complexity grows, this starts to hurt.
Nested callbacks are hard to follow. Dependencies stay hidden inside the nesting. One wrong move during a refactor, and the whole chain breaks.
Testing this kind of code is also difficult. You often need to mock multiple layers of callbacks and manually control timing just to exercise a single code path.
None of this means GCD is bad.
It means GCD was designed to answer a simpler question:
“Where should this code run?”
As applications evolved, we started needing answers to very different questions.
Swift Concurrency: a new mental model
Swift Concurrency doesn’t just introduce new APIs. It introduces a new mental model.
Consider these function signatures:
func loadUser(id: String) async throws -> User
func loadPosts(user: User) async throws -> [Post]Now look at how they are used:
Task {
do {
let user = try await loadUser(id: "123")
let posts = try await loadPosts(user: user)
self.label.text = user.name
} catch {
self.showError(error)
}
}There are no callbacks here.
There’s a single error path.
The flow is linear and easy to follow.
Dependencies are explicit — loadPosts clearly depends on loadUser.
This code reads top to bottom, like synchronous code, but it’s fully asynchronous.
And importantly, this Task can be cancelled.
If the task is cancelled, the work inside it is cancelled as well — automatically.
That’s not just convenience. That’s structure.
The mental shift (this is the important part)
With older concurrency models, the main question was:
❌ “Which queue am I on?”
With Swift Concurrency, the question becomes:
✅ “What task owns this work?”
✅ “Is this work isolated correctly?”
You stop managing threads. You start managing ownership and structure.
Threads — or GCD queues — give you execution:
- “Run this somewhere else”
- “Don’t block the main thread”
DispatchQueue.global().async {
let data = heavyWork()
DispatchQueue.main.async {
updateUI(data)
}
}This answers where code runs.
But threads do not answer:
- Who owns this work?
- How long should it live?
- What happens if I don’t need the result anymore?
- What happens if part of it fails?
- Can it be cancelled?
- Can it safely access shared state?
You have to solve all of that yourself.
Structure means relationships
Structure answers different questions:
- Which work belongs to which feature?
- Which work is a child of other work?
- When the parent ends, what happens to the children?
- How errors propagate
- How cancellation propagates
Threads don’t give you this.
Structured concurrency does.
Task {
let user = try await loadUser()
let posts = try await loadPosts(user)
}This task:
- Has a clear start
- Has a clear end
- Owns its child async calls
- Can be cancelled as a unit
If this task is cancelled:
loadUser()is cancelledloadPosts()is cancelled
That is structure.
Isolation: keeping data safe
Structure alone is not enough.
Once work has clear ownership, data can also have clear ownership.
In older concurrency models, shared mutable state was common. Different threads could read and write the same data at the same time. Making this safe was up to the developer. We used locks, queues, or conventions — and hoped everyone followed them.
Swift Concurrency takes a different approach.
Instead of assuming data can be accessed from anywhere, it assumes the opposite: state should belong somewhere.
That means a piece of mutable state has an owner, and other code can’t just touch it freely. If you want to interact with that state, you do it in a controlled way.
This is why questions like:
- “Who owns this state?”
- “Is this work isolated correctly?”
are so important in Swift Concurrency.
Because ownership is explicit, the compiler can understand more about what your code is doing. It can catch some mistakes earlier — before your code runs. With GCD, this simply wasn’t possible.
Isolation is what makes concurrency safer by default.
This becomes especially relevant in UI code, where you not only want work to have a predictable lifetime — you also want UI state to be updated from a single, safe place.
A practical SwiftUI example
This lack of structure becomes especially visible in UI-driven code, where work is often tied to the lifecycle of a screen.
Without structure:
.onAppear {
DispatchQueue.global().async {
fetchData()
}
}This work:
- Runs independently of the view
- Can outlive the screen
- Must be manually guarded
With structured concurrency:
.task {
await fetchData()
}Now the work is:
- Scoped to the view
- Cancelled automatically when the view disappears
- Tied to a predictable lifecycle
And there’s another subtle benefit: UI updates have a clear home.
UI state is meant to be updated from a single place. In Swift Concurrency terms, you can think of this as isolation: UI-related state belongs to a specific context, and async work updates it in a safe, controlled way.
Structure doesn’t just define how work starts and ends — it also defines how failure behaves.
Errors are part of the structure
With callbacks:
- Errors are just values passed around
- Easy to ignore
- Easy to mishandle
With structured concurrency:
Task {
try await doA()
try await doB()
}If doA() fails:
doB()never runs- The task fails as a whole
Again — structure.
Threads answer where code runs. Structure answers who owns the work, how long it lives, and how it ends.
That’s the real reason Swift Concurrency exists.
Wait, what about Operation Queues?
At this point, it’s fair to ask: what about Operation Queues?
For a long time, I thought they were the answer. They actually tried to solve the "structure" problem years before Swift Concurrency arrived. They gave us dependencies, cancellation, and a clearer way to manage lifecycles.
On paper, they were great. In practice? They always felt a bit clunky to me.
The problem was that the structure only lived at runtime. You had to manually connect the dots between operations, and the compiler didn't really know (or care) what you were doing. You were basically building a house of cards out of closures and hoping you didn't forget to check isCancelled every few lines.
Operation Queues were a library-level band-aid for a language-level problem.
Swift Concurrency takes those same ideas — dependencies and cancellation — and bakes them into the language itself. It’s no longer a tool you're trying to force into your project. It’s part of the air you breathe while writing Swift.
The compiler finally understands what you're trying to do, and it won't let you get it wrong.
Closing
When I stopped trying to memorize Swift Concurrency and started trying to understand why it existed, everything changed.
I didn’t magically stop making mistakes. But I stopped feeling lost. Understanding the reason makes the journey of learning the how far more pleasant.
Best of all? You won’t forget.
That’s what I hope this article does for you too — not teach you syntax, but help you understand the reasons behind Swift Concurrency, so you have a solid starting point and feel inspired to keep learning.
This is the first article on this blog. I hope you enjoyed it. Stay tuned — more to come on this and other topics.