Swift Concurrency Part 1: Async/ Await and Tasks
If you’ve ever been trapped in “callback hell” or fought with nested completion handlers, Swift’s new concurrency model is going to feel like a breath of fresh air.
Let’s walk through the fundamentals async functions, awaiting results, Tasks, and structured concurrency step by step.
Get Sanchit Mehta’s stories in your inbox
Join Medium for free to get updates from this writer.
Why did Apple add async/await?
Before Swift 5.5, asynchronous code in iOS looked like this:
fetchData { data, error in
process(data) { result in
save(result) { success in
print("Done!")
}
}
}That’s hard to read and harder to debug.
async/await fixes this by letting you write asynchronous code in a top-to-bottom style:
let data = try await fetchData()
let result = try await process(data)
try await save(result)
print("Done!")Looks synchronous, but Swift transparently suspends and resumes your function instead of blocking threads.
What actually happens when you use await
Imagine your function runs on a track:
start → do some work → await network call
(pause here)
(resume when data arrives)
→ continue → finishWhen your code hits await, Swift suspends that task and lets other work run.
When the awaited call finishes, the task resumes right after the await line.
Marking a function async
func fetchUser() async throws -> User {
try await Task.sleep(nanoseconds: 1_000_000_000) // fake network delay
return User(name: "Sanchit")
}You call it like this:
Task {
let user = try await fetchUser()
print(user.name)
}Task {} creates a lightweight concurrent job.
Inside that closure, you can use await freely.
Structured concurrency
Swift enforces structure so your tasks have a clear lifetime:
Parent Task
├─ Child Task A
├─ Child Task B
└─ Child Task CIf the parent finishes or is cancelled, all children are automatically cancelled.
No more forgotten background work leaking forever.
Task vs Task.detached
Task { }
- Inherits context keeps the same priority, actor, and cancellation state as the parent.
- Runs within the current task hierarchy (e.g., same structured concurrency scope).
Task.detached { }
- Does not inherit context runs completely independent of the parent task.
- Has its own priority, actor, and cancellation parent cancellation won’t affect it.
Running tasks in parallel with async let
You can launch multiple async operations simultaneously:
async let user = fetchUser()
async let posts = fetchPosts()
let (u, p) = await (try user, try posts)Both run concurrently, and you wait for them later simple and efficient.
Handling errors and cancellation
Async functions can also throw:
func loadImage() async throws -> UIImageWhen you try await, you either get the value or an error:
do {
let image = try await loadImage()
} catch {
print("Failed: \(error)")
}To cancel a running task:
let task = Task { try await longWork() }
task.cancel()Inside longWork, check for cancellation:
try Task.checkCancellation()Cancellation is cooperative your code must check for it






