All articles
iOS6 min read

Swift async let: The One Keyword That Makes Your App 3x Faster

Sequential async calls are killing your load times. async let runs them all at once — same syntax, way faster results.

K
Karan Pal
Author
Image Generated by AI
Image Generated by AI

🚀 Enter async let

Here’s the thing about regular await—it's polite. Too polite.

When you write let data = await fetchData(), your code stops and waits until it receives data. Nothing else is going to happen until that call finishes. Then it moves on to the next line.

And that’s completely fine if tasks depend on each other. But what if they don’t?

let store = await fetchStore()        // waits 2 seconds
let products = await fetchProducts()      // waits 3 seconds
let shelves = await fetchShelves()  // waits 2 seconds
// total: 7 seconds

Those three calls don’t need each other. Store data doesn’t depend on products. Product don’t need shelves. They could all run at the same time.

Enter async let.

async let store = fetchStore()        // starts immediately
async let products = fetchProducts()      // starts immediately
async let shelves = fetchShelves()  // starts immediately

let storeData = await store           // wait for all three
let productsData = await products
let shelvesDat = await shelves
// total: 3 seconds (the longest one)

Same three calls. 3 seconds instead of 10. That’s the magic — async let kicks off the task but doesn't wait for it. Instead, it waits later if required, and only when you actually need the result.

Think of it like ordering at a food court. If you have to go sequentually, you order pizza, wait for it, then order a drink, wait for it, then order fries. On the other hand, you order all three at once, then wait at the pickup counter for everything together.

The syntax is dead simple:

That’s it. One keyword swap and your parallel execution is done.

No Task Groups. No manual continuation management. Just slap async before let and you're running in parallel.

**Swift For-Await Loops: Handle Real-Time Data Without the Chaos** _Network streams, notifications, sensor data — for-await loops make async sequences feel like normal Swift code…_swift-pal.comhttps://swift-pal.com/swift-for-await-loops-handle-real-time-data-without-the-chaos-e1f2d8d98f30

⏱️ Real Performance Comparison

Let’s prove this with actual code you can run. We’ll simulate three API calls, each taking different amounts of time.

import Foundation

// fake API calls with delays
func fetchUser() async -> String {
    try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 sec
    return "User: Alice"
}

func fetchPosts() async -> String {
    try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 sec
    return "Posts: 42"
}

func fetchFriends() async -> String {
    try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 sec
    return "Friends: 150"
}

// Sequential version (slow)
func loadDataSequential() async {
    let start = Date()

    let user = await fetchUser()
    let posts = await fetchPosts()
    let friends = await fetchFriends()

    let elapsed = Date().timeIntervalSince(start)
    print("\(user), \(posts), \(friends)")
    print("Sequential time: \(elapsed) seconds")
}

// Parallel version (fast)
func loadDataParallel() async {
    let start = Date()

    async let user = fetchUser()
    async let posts = fetchPosts()
    async let friends = fetchFriends()

    let userData = await user
    let postsData = await posts
    let friendsData = await friends

    let elapsed = Date().timeIntervalSince(start)
    print("\(userData), \(postsData), \(friendsData)")
    print("Parallel time: \(elapsed) seconds")
}

// run both
Task {
    await loadDataSequential()
    print("---")
    await loadDataParallel()
}

Run this and you’ll see:

User: Alice, Posts: 42, Friends: 150
Sequential time: 7.2952070236206055 seconds
---
User: Alice, Posts: 42, Friends: 150
Parallel time: 3.0890719890594482 seconds

The parallel version finishes in the time of the slowest task (3 seconds for posts). The sequential version adds them all up (2+3+2 = 7 seconds).

That’s not a micro-optimization. That’s user-noticeable performance.

Now here’s something cool — you can mix and match. If one task depends on another, use regular await. For independent stuff, use async let.

let userId = await fetchUserId()           // need this first
async let profile = fetchProfile(userId)   // can start now
async let settings = fetchSettings(userId) // can start now

let userData = await profile
let userSettings = await settings

User ID has to come first. But once you have it, profile and settings can run together.

🛠️ Practical Use Cases

Okay, so when does this actually matter in real apps?

• Dashboard Loading

Loading a user dashboard — you need user info, notifications, recent activity, maybe weather data. None of these depend on each other.

func loadDashboard() async -> Dashboard {
    async let profile = api.fetchProfile()
    async let notifications = api.fetchNotifications()
    async let activity = api.fetchRecentActivity()
    async let weather = api.fetchWeather()

    return Dashboard(
        profile: await profile,
        notifications: await notifications,
        activity: await activity,
        weather: await weather
    )
}

Four API calls in parallel instead of sequence. If each takes 1 second, you just saved 3 seconds of loading time.

• Batch Image Processing

Downloading multiple images for a gallery:

func loadGallery(urls: [URL]) async -> [UIImage?] {
    // don't do this - sequential
    // var images = [UIImage]()
    // for url in urls {
    //     let image = await downloadImage(from: url)
    //     images.append(image)
    // }

    // do this - parallel (kind of)
    await withTaskGroup(of: UIImage?.self) { group in
        for url in urls {
            group.addTask {
                await downloadImage(from: url)
            }
        }

        var images = [UIImage?]()
        for await image in group {
            images.append(image)
        }
        return images
    }
}

Wait, that’s not async let — that’s Task Groups. Yes, for dynamic collections you need Task Groups. async let works best when you know exactly how many tasks you have at compile time.

Let me show you the async let version for a fixed set:

func loadProfileImages() async -> (UIImage?, UIImage?, UIImage?) {
    async let avatar = downloadImage(from: avatarURL)
    async let cover = downloadImage(from: coverURL)
    async let thumbnail = downloadImage(from: thumbnailURL)

    return (await avatar, await cover, await thumbnail)
}

Three specific images = async let

Unknown number of images = Task Groups

• Multi-Source Data Aggregation

Fetching data from different services to build one view:

func loadProductDetails(id: String) async -> ProductView {
    async let product = database.fetchProduct(id)
    async let reviews = reviewService.fetchReviews(id)
    async let inventory = inventoryAPI.checkStock(id)
    async let recommendations = mlService.getRecommendations(id)

    return ProductView(
        product: await product,
        reviews: await reviews,
        inStock: await inventory,
        similar: await recommendations
    )
}

Each service is independent. Run them all at once.

One gotcha though — if any of these throw errors, you need to handle them individually:

async let product = database.fetchProduct(id)
async let reviews = reviewService.fetchReviews(id)

do {
    let productData = try await product
    let reviewData = try await reviews
    // use both
} catch {
    // handle errors
}

All async let tasks start immediately, even inside the do block. The try await is where you actually wait for results.

🎯 async let vs Task Groups

So when do you use async let vs Task Groups? Both do parallel execution, but they fit different scenarios.

Use async let when:

async let user = fetchUser()
async let posts = fetchPosts()
async let settings = fetchSettings()

Clean. Obvious. Each task has a name.

Use Task Groups when:

await withTaskGroup(of: String.self) { group in
    for id in userIDs {
        group.addTask { await fetchUser(id) }
    }

    for await result in group {
        process(result) // handle as each finishes
    }
}

Task Groups give you more control — you can cancel individual tasks, handle results as they arrive, set priorities. But they’re more verbose.

Here’s the decision tree:

You can even mix them:

async let metadata = fetchMetadata()

let items = await withTaskGroup(of: Item.self) { group in
    for id in ids {
        group.addTask { await fetchItem(id) }
    }
    // collect results
}

let meta = await metadata
return (meta, items)

One fixed task (metadata) with async let, dynamic tasks (items) with Task Group.

Actually — one more thing about async let. The tasks are child tasks of the current task. If the parent gets cancelled, all async let tasks cancel too. That’s usually what you want, but worth knowing.

let task = Task {
    async let a = longRunningTask()
    async let b = anotherLongTask()

    let resultA = await a
    let resultB = await b
}

task.cancel() // cancels both a and b

Automatic cleanup. Nice.

💡 Wrap-Up

async let = parallel execution without the ceremony.

Use it when:

Skip it when:

The syntax is just:

async let x = asyncFunction()  // starts now
let result = await x           // wait when needed

Performance win? Massive. Instead of adding up all task times, you wait for the longest one. Three 2-second calls? 2 seconds total, not 6.

One gotcha: all async let tasks start immediately when you declare them, not when you await them. So don’t declare them if you’re not sure you need them.

async let data = expensiveCall()  // already running!
if someCondition {
    let result = await data  // only use it conditionally
}
// task runs even if condition is false - wasteful

Better to check conditions first, then declare async let tasks you’ll definitely use.

📺 Want to see this with real examples and live coding? Check out the complete tutorial on Swift Pal: https://youtube.com/@swift-pal

That’s async let. One keyword. Way faster apps. Finally running things in parallel without the Task Group boilerplate.

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading