Swift 5.5 in 2026: What Async/Await Actually Means for Your App

Swift 5.5 async/await lets you write asynchronous code that reads top to bottom, so network calls and database work stop nesting inside completion handlers. Errors propagate with try, and the compiler checks them. You do not need a full rewrite. Migrate new code first, then convert old handlers as you touch them.
Part of the complete app development guide.
On this page
- The headline feature: async/await
- Actors and the data race problem
- What changed between 5.5 and now
- Do you need to migrate?
- Common mistakes I still see
- Blocking the main actor
- Assuming await means background
- Fire-and-forget tasks that outlive their view
- Where this leaves you in 2026
- FAQ
- Is Swift 5.5 still relevant in 2026?
- Can I use async/await with old callback-based libraries?
- Do I have to migrate my whole app to async/await?
Swift 5.5 shipped back in 2021, and if you’re still running an app built around completion handlers and DispatchQueue, you’re not alone. Plenty of production codebases never fully migrated, and that’s fine. What matters is knowing which parts of that release actually changed how you write iOS code today, and which parts you can safely ignore.
I’ve spent the last several years building and maintaining iOS apps, much of it through Long Island app development work for small teams that don’t have the luxury of a full rewrite. So this is a practical look, not a feature tour.

The headline feature: async/await
Before Swift 5.5, a network call looked like this:
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
let url = URL(string: "https://api.example.com/users/(id)")!
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else { return }
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(error))
}
}.resume()
}
After 5.5, the same thing:
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)
}
That’s the whole pitch. Fewer nesting levels, errors propagate with try, and the compiler enforces that you handle failure. If your app does a lot of chained network calls, this alone is worth the migration.

Actors and the data race problem
Swift 5.5 also introduced actors, which serialize access to their own state. This was the beginning of what became Swift’s strict concurrency checking, and by 2026 that’s no longer optional in a lot of projects. Xcode’s Swift 6 language mode turns data races into compile errors.
actor ImageCache {
private var store: [String: Data] = [:]
func image(for key: String) -> Data? {
store[key]
}
func set(_ data: Data, for key: String) {
store[key] = data
}
}
You call those methods with await from outside the actor. The compiler guarantees two threads can’t touch store at the same time. If you’ve ever chased a crash that only reproduced on a physical device under load, this is the fix.
What changed between 5.5 and now
Worth being honest here: the 5.5 version of concurrency was rough. A few things that were painful then and are better now:
- Sendable checking was lenient. Now it’s strict, and you’ll get warnings (or errors) about passing non-Sendable types across isolation boundaries.
- Task groups existed but the ergonomics improved.
withTaskGroupis still the tool for running N things in parallel. - @MainActor inference got smarter, so you write fewer explicit annotations in SwiftUI views.
- AsyncSequence matured into something people actually use for streams, not just a demo.
Do you need to migrate?
Not necessarily, and anyone who tells you otherwise is selling something. Here’s how I decide:
- New code: async/await, always. There’s no reason to start a completion handler in 2026.
- Stable old code: leave it. A working networking layer from 2019 doesn’t need rewriting just because the syntax is newer.
- Code you’re touching anyway: migrate as you go. If you’re already in a file fixing a bug, convert the callbacks while you’re there.
- Anything hitting Swift 6 language mode: you’ll migrate whether you want to or not, because the compiler will make you.
The bridge between the two worlds is withCheckedContinuation, and it’s genuinely useful when you have to call old callback-based APIs from new async code:
func legacyFetch(id: Int) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
fetchUser(id: id) { result in
continuation.resume(with: result)
}
}
}
One warning: if you forget to call resume on every path, that task hangs forever. There’s a runtime check that’ll complain, but it only fires in debug. I’ve seen this bite people in production code paths that were never exercised in testing.
Common mistakes I still see
Blocking the main actor
Marking a function @MainActor and then doing heavy JSON parsing inside it will freeze your UI. Do the work off the main actor, then hop back to update state.
@MainActor
func load() async {
let raw = await fetchRawData() // off main
let parsed = await parse(raw) // still off main
self.items = parsed // back on main
}
Assuming await means background
await suspends, it doesn’t necessarily switch threads. If the function you’re awaiting is @MainActor, you’re still on the main thread. This trips up people coming from GCD, where the mental model was different.
Fire-and-forget tasks that outlive their view
If you kick off a Task { } inside a SwiftUI view and the view disappears, that task keeps running unless you cancel it. Store the task and cancel it in onDisappear, or use .task { } which handles cancellation for you.
Where this leaves you in 2026
Swift 5.5 was the point where Apple committed to making concurrency a language feature instead of a library convention. Five years on, that bet paid off, but it also means the migration debt is real and the compiler is no longer letting it slide. If you’re on a team that’s been putting this off, the practical move is to enable strict concurrency checking on one module at a time, fix what breaks, and ship. You don’t need a big rewrite weekend.
FAQ
Is Swift 5.5 still relevant in 2026?
Yes, as the starting point for async/await and actors. The syntax you write today is a direct descendant, and understanding where it came from helps when you hit Sendable errors.
Can I use async/await with old callback-based libraries?
Yes. Wrap them with withCheckedThrowingContinuation. Just make sure every code path calls resume exactly once, or the task will hang.
Do I have to migrate my whole app to async/await?
No. Migrate new code and anything you’re already editing. Forcing a full rewrite on a working app usually costs more than it saves.


