Using Swift async/await for Clear Network Requests in iOS 15+
Learn how Swift’s async/await simplifies network code, see a concrete fetchUser example, and understand the deployment‑target trade‑off.
28 May 2026, 21:00 UTC

Problem: Callback‑heavy network code hurts readability
When fetching data with URLSession’s completion handlers, each network call introduces a nesting level and forces error handling into separate closures. As the number of sequential requests grows, the code becomes hard to follow and easy to miss error cases.
Thesis: async/await lets you write network calls that look sequential while staying non‑blocking
Swift 5.5’s async/await transforms an asynchronous function into a continuation‑based state machine. The compiler handles thread switching, so you can write await expressions that appear to block but actually free the thread until the awaited operation completes.
How async/await works under the hood
An async function is compiled into a struct that stores its local state and a continuation pointer. When an await point is reached, the function returns to the caller, preserving its stack via the continuation. When the awaited operation finishes, the continuation resumes the function on an appropriate thread. This eliminates manual callback chaining while preserving concurrency.
Worked example: fetching a JSON payload
struct User: Decodable {
let id: Int
let name: String
}
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Call site
Task {
do {
let user = try await fetchUser(id: 42)
print("User: \(user.name)")
} catch {
print("Failed: \(error)")
}
}
Place this code in any Swift file of an iOS/macOS project. No special entitlements are required beyond standard network access. Run the app on a simulator or device with iOS 15+ (or macOS 12+) to see the user name printed in the console.
Verification steps
- Open Xcode 14 or later (Swift 5.5 toolchain) and build the project.
- Run on a simulator; watch the console for the printed user name or an error message.
- Open Debug → View Debugging → Concurrency inspector. You should see a task that suspends at the
await URLSession.shared.data(from:)line and resumes when the data arrives. - To check the back‑deployment impact: set the deployment target to iOS 13, add the
SwiftConcurrencylibrary via Xcode’s project settings, then open the Report Navigator → Size tab. Compare the binary size with and without the library; you should observe an increase of roughly 1 MB.
Trade‑offs and limitations
- The minimum deployment target for native async/await is iOS 15/macOS 12. Supporting older systems requires the concurrency back‑deployment library, which adds approximately 1 MB to the app bundle.
- Mixing async/await with legacy delegate‑based APIs (e.g.,
URLSessionDownloadDelegate) can lead to threading mistakes if callbacks are not marked with@MainActoror executed viaTask.detached. - Enabling strict concurrency checking (Swift 5.6+) may surface data‑race warnings; adopt it incrementally to avoid being overwhelmed.
Actionable closing
Begin by converting a single network helper to async/await, verify its behavior with the Concurrency inspector, and then extend the pattern throughout your codebase. Keep the deployment target in mind; if you must support older OS versions, include the back‑deployment library and measure its size impact early in the process.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.