Async/Await: Essence, Async Functions and Working with await

Author: IT Sectr Published: 2026-03-16 Reading time: 8 min

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 syntactic sugar for asynchronous code without nested callbacks
  • async marks a function as asynchronous and suspendable
  • await suspends the function execution until the async operation completes
  • Task in Swift creates a new asynchronous unit of work with context
  • Structured Concurrency guarantees that all child tasks complete before the parent

What is Async/Await and Asynchronous Programming

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.

Operating Principle: Suspendable Functions

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.

How Async/Await Works: Suspension and Resumption

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.

  • Swift — the compiler translates the async function into SIL with a runAsync coroutine frame
  • Kotlin — each suspend function receives a Continuation and delegates execution to the dispatcher
  • Both languages — free the thread at await, not blocking it during waiting
  • Exceptions — are propagated through standard mechanisms (throws / try-catch)

Async/Await vs Callbacks and GCD: Approach Comparison

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.

AspectCallbackGCDAsync/Await
NestingDeep (pyramid of doom)Medium (groups+notify)Linear (flat)
Error HandlingPer-callbackManualUnified (try/catch)
CancellationManualLimitedTask.cancel()
Thread SafetyManualVia serial queueVia 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.

Swift Concurrency: Task, TaskGroup and Actor

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.

Structured Concurrency

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.

Async/Await in Kotlin with Coroutines

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.

Code Examples with Async/Await in Swift and Kotlin

Let's look at two examples for each language: sequential data loading and parallel requests with TaskGroup or async/await.

Swift: Sequential async Calls

await suspends execution until user data is received, then until friends are received. No nested closures.

swift
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)
}

Swift: Parallel Requests with TaskGroup

TaskGroup launches tasks in parallel and collects results into an array. The order of results may not match the order of launch.

swift
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
    }
}

Kotlin: Sequential Call with async/await

suspend functions can call other suspend functions. Analogous to Swift: sequential calls.

kotlin
suspend fun loadProfile(): Profile {
    val user = api.fetchUser()
    val friends = api.fetchFriends(user.id)
    return Profile(user, friends)
}

Kotlin: Parallel Requests with async/await

In Kotlin, parallelism is achieved through coroutineScope with async for each request.

kotlin
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())
}

Common Mistakes and Limitations of Async/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.

Excessive use of async without necessity

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.

Blocking the main thread via .result

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.

Task leak on cancellation

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

How is async/await different from DispatchQueue?

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.

Does await block the thread?

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.

Can an async task be cancelled?

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().

What is MainActor in Swift?

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.

Which languages support async/await?

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

  • Async/Await is a language construct for linear asynchronous code without callback hell
  • Swift Concurrency provides Task, TaskGroup, Actor, and Structured Concurrency
  • Kotlin coroutines use suspend functions, Dispatchers, and coroutine scopes
  • Suspension without blocking is the key difference of async/await from threads and queues
  • Structured Concurrency guarantees that all child tasks complete before the parent
  • Async/Await vs GCD — the former is preferable for new code, the latter for legacy and low-level
  • MainActor in Swift and Dispatchers.Main in Kotlin protect the UI thread from background operations

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.

Discuss the project

Read also