Swift

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

By Ken Key Jul 8, 2024 4 min read Updated Oct 16, 2026

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

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

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.

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

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.

Swift 5.5 in 2026: What Async/Await Actually Means for Your App: Actors and the data race problem

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. withTaskGroup is 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.

Have a project like this?

I do Long Island app development for Long Island businesses. If you need this done right, let’s talk.