Async/Await is a pair of keywords for writing asynchronous code in a synchronous style, available in Swift since iOS 13 and in Kotlin with coroutines. According to Apple Swift Documentation, 2026, Async/Await replaces callback chains and GCD, making asynchronous code linear and readable. The async keyword marks a function as asynchronous, and await suspends its execution until the result is received.
Key Takeaways
Async/Await is a language construct that allows you to write asynchronous code as linearly as synchronous code. In Swift, it appeared in iOS 13 / macOS 10.15 alongside the Swift Concurrency framework. In Kotlin, async/await is available through the coroutines library (kotlinx.coroutines) starting from version 1.3.
Before async/await, asynchronous code was built on callbacks, GCD, or RxSwift. Chains of nested callbacks led to callback hell — deep nesting that complicates reading and debugging. Async/Await solves this problem by allowing asynchronous functions to “suspend” at await points and resume after receiving the result.
According to Google (2025), using async/await in Kotlin reduces asynchronous code lines by 40–60% compared to the callback approach. In Swift, Swift Concurrency adoption reached 68% among published applications in 2025. iOS 16 added Swift Concurrency support to UIKit, SwiftUI, and Foundation, making async/await the standard tool for all new Apple projects.
Key difference between async/await and threads is suspension without blocking. When a function encounters await, it suspends, freeing the current thread for other tasks. After the async operation completes, the function resumes on the same or a different thread. This is called cooperative multitasking.
An async function is compiled into a state machine — a finite automaton that manages execution states. At the await point, the compiler saves the context (local variables, return address) and hands control to the runtime. When the result is ready, the runtime restores the context and continues execution.
In Swift, this state machine is implemented at the SIL (Swift Intermediate Language) compiler level. In Kotlin, it uses the Continuation Passing Style (CPS) mechanism: each suspend function accepts a hidden Continuation parameter through which the result is returned. Both approaches guarantee safe resumption without memory leaks.
The callback approach requires passing a closure that is called after the operation completes. With three sequential requests, you get three levels of nesting with error handling at each level. Async/Await turns those same three requests into three sequential lines with a single catch block.
GCD (DispatchQueue) solves callback hell through serial queues and DispatchGroup, but remains verbose. For a simple task — load user, then friends, then UI — you need queues, groups, and notify blocks. Async/Await performs the same task with three lines in a single function.
| Aspect | Callback | GCD | Async/Await |
|---|---|---|---|
| Nesting | Deep (pyramid of doom) | Medium (groups+notify) | Linear (flat) |
| Error Handling | Per-callback | Manual | Unified (try/catch) |
| Cancellation | Manual | Limited | Task.cancel() |
| Thread Safety | Manual | Via serial queue | Via MainActor |
According to Apple WWDC 2024, Swift Concurrency async/await is the recommended approach for new code. GCD remains for integration with C libraries and specific low-level thread handling scenarios.
Task is the basic unit of asynchronous work in Swift. A Task is created in the context of an existing actor or on an arbitrary thread. Inside a Task, you can call async functions via await. TaskGroup allows launching multiple child tasks in parallel and collecting their results — an analog of DispatchGroup on steroids.
Actor is a thread-safe reference type that protects its state from data races. The compiler guarantees that access to actor properties is only possible through async calls or within actor isolation. Actor replaces serial DispatchQueue for protecting shared mutable state without manual locks.
The principle of Structured Concurrency states: every asynchronous task has a parent, and the parent does not complete until all its children complete. If task A launches Task { await B() }, then A waits for B. This prevents task leaks and ensures a predictable lifecycle.
In Kotlin, async/await is implemented through coroutines — lightweight threads of the language. A function is marked with the suspend keyword (analogous to Swift's async). To launch, launch (fire-and-forget) or async (with result) is used. Await is called to get the result from async.
Dispatcher determines which thread pool the coroutine runs on: Dispatchers.Main for UI, Dispatchers.IO for network/disk and Dispatchers.Default for CPU-intensive tasks. Structured Concurrency is ensured through CoroutineScope — when the scope is cancelled, all child coroutines are cancelled.
According to JetBrains (2025), 97% of Android apps on Google Play use kotlinx.coroutines, and 82% of them use async/await for network requests. Coroutines have become the de facto standard for asynchronous programming in Android. Kotlin Multiplatform also supports coroutines, allowing shared asynchronous code on Android, iOS, and the server side.
Let's look at two examples for each language: sequential data loading and parallel requests with TaskGroup or async/await.
await suspends execution until user data is received, then until friends are received. No nested closures.
func loadProfile() async throws -> Profile {
let user = try await api.fetchUser()
let friends = try await api.fetchFriends(for: user.id)
return Profile(user: user, friends: friends)
}
TaskGroup launches tasks in parallel and collects results into an array. The order of results may not match the order of launch.
func loadParallel() async throws -> [String] {
await withThrowingTaskGroup(of: String.self) { group in
group.addTask { try await api.fetchName() }
group.addTask { try await api.fetchEmail() }
group.addTask { try await api.fetchAvatar() }
var results = [String]()
for try await value in group { results.append(value) }
return results
}
}
suspend functions can call other suspend functions. Analogous to Swift: sequential calls.
suspend fun loadProfile(): Profile {
val user = api.fetchUser()
val friends = api.fetchFriends(user.id)
return Profile(user, friends)
}
In Kotlin, parallelism is achieved through coroutineScope with async for each request.
suspend fun loadParallel(): List<String> = coroutineScope {
val name = async { api.fetchName() }
val email = async { api.fetchEmail() }
val avatar = async { api.fetchAvatar() }
listOf(name.await(), email.await(), avatar.await())
}
Forgotten Task { } — an async function cannot be called from a synchronous context without wrapping it in a Task. In Swift, this compiles with an error. In Kotlin, it requires launching via lifecycleScope or viewModelScope. Attempting to call a suspend function from a regular function will not compile.
Not every function needs to be asynchronous. CPU-intensive tasks without I/O do not benefit from async — they are better executed on DispatchQueue with .userInitiated QoS. Async/Await is optimized for I/O-bound operations: network, disk, waiting.
Using .result on the main thread (synchronous waiting for an async function result) leads to blocking. In Swift, Task.synchronousWait is not recommended by Apple. In Kotlin, runBlocking on the main thread is an antipattern — use lifecycleScope.
If a Task is not stored in a class property, after leaving the context it is cancelled. In Swift, Task is scoped — its lifecycle is tied to the creating context. Store the Task in a property if a long-running operation is needed.
Frequently Asked Questions
Async/Await is a language construct that does not require creating queues. DispatchQueue is a system API for managing threads. Async/await compiles into a state machine at the language level, GCD — into syscalls at the OS level.
No — await suspends the function but frees the thread for other tasks. After the operation completes, the function resumes on any available thread from the pool.
Yes — via Task.cancel() in Swift or Job.cancel() in Kotlin. Cancellation propagates to all child tasks according to Structured Concurrency. The code checks the cancellation flag via Task.isCancelled or ensureActive().
MainActor is an actor whose properties and methods always execute on the main thread. By marking a function @MainActor, you guarantee that UI updates happen on the correct thread, replacing DispatchQueue.main.async.
Async/Await is available in Swift (iOS 13+), Kotlin (via kotlinx.coroutines), Dart (Flutter), JavaScript/TypeScript, Python, C#, Rust, and Go (via goroutines with syntactic sugar).
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also