Mastering async/await in Swift: A Beginner’s Guide to Modern Concurrency (Part 1)
Confused by Swift’s async/await? Learn the basics of async/await, how it improves your Swift code, and how to get started with real…

🚀 Introduction
Remember the dark times when every network call came with a nested completion handler… which came with another completion handler… which came with an existential crisis? 😩
Well, Swift developers, rejoice — those callback pyramids are getting a makeover! 🎉
With Swift 5.5, Apple introduced async/await, a modern concurrency model that makes your asynchronous code look synchronous, feel cleaner, and play nicer with error handling.
In this three-part series, I’ll help you truly master Swift’s async/await:
- 👉 Part 1 (you’re here!): Understand the fundamentals of async/await and how to get started
- 👉 Part 2: Dive into real-world use cases like API calls, image loading, and chained async operations
- 👉 Part 3: Explore advanced patterns like cancellation, task management, and async gotchas
By the end of this series, you’ll be ready to ditch callback hell and write code that’s clean, scalable, and future-proof.
⚠️ Note: async/await is available starting _Swift 5.5+_ (iOS 13+ with back-deployment, iOS 15+ natively).
Ready? Let’s bring some calm to your concurrency chaos.
🧠 What is async/await in Swift (and Why Should You Care)?
Swift’s async/await is part of a broader shift toward structured concurrency — a fancy term for “make async code less chaotic and more readable.”
But let’s break it down without the buzzwords:
🧵 So, what is async?
When you mark a function as async, you’re telling Swift:
“Hey, this function does something that takes time — like fetching data from a server or reading a file — so don’t block the main thread while it happens.”
Instead of waiting like a clingy ex, Swift just… moves on. 🚶
func fetchUserProfile() async -> User {
// pretend this is doing something time-consuming
}This function can now run asynchronously — meaning it won’t block your app’s main thread while it completes its task.
⏳ And what does await do?
When you call an async function, you use await to say:
“Wait here until I get the result — but only pause _this_ function, not the whole app!”
let user = await fetchUserProfile()This keeps your UI smooth and responsive, while your app does time-consuming work in the background. No more GCD spaghetti. 🍝
🤝 async/await vs completion handlers
Let’s compare:
Old way: Completion handlers
fetchUser { result in
switch result {
case .success(let user):
print(user.name)
case .failure(let error):
print("Oops: \(error)")
}
}New way: async/await
do {
let user = try await fetchUser()
print(user.name)
} catch {
print("Oops: \(error)")
}💡 See that? Same logic, but now it reads top-down — like regular code. Much easier to write, debug, and actually understand at 2AM.
📌 Summary
- async lets a function run without blocking the current thread
- await suspends only your function while waiting for the result
- Together, they make code readable and sane again
- You can finally ditch DispatchQueue.global().async {} for good (well, mostly 😅)
✍️ Writing Your First async/await Function in Swift
Time to get our hands dirty and see how async/await really works under the hood — with zero third-party libraries and no mental gymnastics. Just pure Swift.
🛠 Step 1: A Synchronous Function (The Old School Way)
Let’s start with something familiar. Here’s a function that simulates fetching a user from a local database:
func getUserName() -> String {
return "Swift Pal"
}Simple. Straightforward. But let’s say this is now a network call, and you can’t just return it instantly. You’ll need to wait. And waiting = async.
🧪 Step 2: Make it Async
Let’s simulate a network delay using Task.sleep, and then convert this function to be async:
func getUserName() async -> String {
// Sleep for 2 seconds (in nanoseconds!)
try? await Task.sleep(nanoseconds: 2_000_000_000)
return "Swift Pal"
}✅ The async keyword says: “This function is now asynchronous.”
✅ The await inside Task.sleep says: “Hold on here, this takes time.”
🧵 Step 3: Call It from a Task
You can’t just call await from a regular function — you need to be in an asynchronous context. One way to do this is by using a Task block:
Task {
let name = await getUserName()
print("Hello, \(name) 👋")
}Boom 💥 — this prints after 2 seconds, without freezing your UI. Feels like magic, works like logic ✨
🧯 Bonus: What if You Need to Throw?
Let’s say your function might fail (e.g. network issue). Just add throws like this:
func fetchProfile() async throws -> String {
let success = Bool.random()
if success {
return "Async Swift 🧑💻"
} else {
throw URLError(.badServerResponse)
}
}And you call it like this:
Task {
do {
let profile = try await fetchProfile()
print("Fetched: \(profile)")
} catch {
print("Error fetching profile: \(error)")
}
}🧠 TL;DR
- Add async to your function to make it non-blocking
- Use await for things that take time
- Wrap the call inside a Task {} if you’re calling it from a non-async context
- Throw errors just like you would in sync code — it’s Swift-y as ever
Next, I’ll show how async/await completely transforms a messy, real-world API call written with GCD into something your future self will thank you for 🙏
🔄 Refactoring GCD Code to async/await: A Real Example
Now that you’ve got a grip on async and await, let’s see how this actually helps you in real Swift codebases — by replacing some good old-fashioned GCD spaghetti 🍝
Think of this section as:
“How would I refactor existing code that uses DispatchQueue and completion handlers?”
👴 The Legacy Way — Using GCD
Let’s say you have a method that simulates an API call using DispatchQueue:
func fetchUserName(completion: @escaping (String?, Error?) -> Void) {
DispatchQueue.global().async {
sleep(2) // simulate network delay
let success = Bool.random()
DispatchQueue.main.async {
if success {
completion("Swift Pal", nil)
} else {
completion(nil, URLError(.badServerResponse))
}
}
}
}And you’d call it like this:
fetchUserName { name, error in
if let error = error {
print("❌ Error: \(error)")
} else if let name = name {
print("✅ Fetched user: \(name)")
}
}It works. But it’s noisy. And that pyramid will only grow taller once you add more async steps.
✨ Refactored with async/await
Here’s the upgraded version using Swift concurrency:
func fetchUserName() async throws -> String {
try await Task.sleep(nanoseconds: 2_000_000_000) // simulate delay
let success = Bool.random()
if success {
return "Swift Pal"
} else {
throw URLError(.badServerResponse)
}
}Calling it becomes a joy:
Task {
do {
let name = try await fetchUserName()
print("✅ Fetched user: \(name)")
} catch {
print("❌ Error: \(error)")
}
}🧘 Why This is Better (Beyond Just Syntax)
- No thread juggling: No more jumping between DispatchQueue.global() and .main
- Clearer error flow: try + catch > passing optional errors
- Much easier to test: No escaping closures to deal with
- Easier to scale: Want to chain calls? You won’t need nested closures anymore
🧠 Real Talk:
When you refactor even one old networking method like this, you’ll start seeing how much mental overhead GCD had been silently adding all along.
And that’s the whole point of structured concurrency — which I’ll cover next 🧵
📌 Structured Concurrency in One Minute (Promise)
Okay, you’ve heard the term. Apple loves using it. But what is structured concurrency, and why does it sound like something from an advanced computer science class?
Here’s the non-boring version:
🧵 What is Structured Concurrency?
Structured concurrency is Swift’s way of saying:
“Let’s keep async tasks _organized_, _scoped_, and _tied to something_ — so they don’t wander off and cause chaos.”
In other words, tasks that you spawn should:
- Start where you can see them 👀
- Finish when you’re done ✅
- Not silently leak memory or crash your app 😬
Think of it like this:
Without Structure (Bad old days):
- “Fire and forget” background threads flying everywhere
- Easy to leak memory, forget to cancel, or crash the app
- Debugging async bugs is a nightmare
With Structure (Swift’s async/await):
- Tasks are scoped and behave like polite guests 🫡
- Easier to manage memory and cancellation
- Debugging is more predictable and readable
🏛️ Example: Task Group vs Detached Task
🚫 Detached Task (unstructured):
Task.detached {
// Not tied to any parent task
await doSomething()
}This is a wild child. If it fails or leaks, you may not notice until things break.
✅ Structured Task:
Task {
await doSomething()
}This task is attached to its parent, such as a view or calling scope. When that parent is cancelled or deallocated — this task goes with it.
This is what makes Swift’s async/await model so safe and scalable.
🤯 TL;DR
- Structured concurrency = “async with guard rails”
- It scopes and contains your async tasks
- It makes memory management and task cancellation so much easier
- You’ll use it every time you use Task {} or async let (coming in Part 2 👀)
🎬 Wrapping Up: async/await Just Clicked, Didn’t It?
If you’ve made it this far — congrats, you’re no longer afraid of the async monster hiding in your Swift codebase 😌🎉
Here’s what you’ve learned in Part 1:
- What async and await really mean (without sounding like a CS lecture)
- How to write your first async function
- How to refactor GCD mess into clean, testable Swift code
- The basics of structured concurrency — Swift’s way of making async less… unhinged 🧘♂️
You now understand the “why” and “how” — but we’re just getting warmed up 🔥
👀 Part 2 — Real-World Examples
In Part 2, we’ll get hands-on with:
- Making real API calls using URLSession
- Downloading images asynchronously (without blocking the UI)
- Chaining and parallelizing async tasks
- Handling async errors like a pro
Basically, I’ll take everything you learned here and actually build stuff with it. No more theory — just real code you can use in production 👨💻💪
**Mastering async/await in Swift: Real-World Examples for iOS Developers (Part 2)** _Learn how to use async/await in your actual iOS projects — from network calls and image loading to chaining and…_medium.comhttps://medium.com/swift-pal/mastering-async-await-in-swift-real-world-examples-for-ios-developers-part-2-b3e43b1c27e7
🎉 Enjoyed this article? Your support means the world to me!
💬 Drop a comment below! I love hearing about your experiences and answering questions
🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal
💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal
Happy coding! 🚀
New articles, straight to your inbox.
No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.
Keep reading
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read