Why Your App Freezes : Async and Await Basics

What Are Asynchronous Operations?

Asynchronous operations are tasks that run in the background without blocking the current thread. This means your app can continue responding to user interactions while waiting for these tasks to complete.

These operations are especially useful when performing time-consuming or slow tasks — so your app stays smooth and responsive.


Common Examples of Asynchronous Tasks:

  1. Network Requests
    Fetching or sending data to a server (like loading user profiles, downloading images, or syncing data).

  2. File I/O
    Reading or writing large files to disk (such as saving photos or loading documents).

  3. Timers & Delays
    Waiting for a specific amount of time before executing a task (e.g., countdowns, scheduled notifications, retry delays).


When to Use “async”?

When you mark a function as async, you're telling Swift two important things:

  • "This function might take some time to finish."
    It could be doing something like downloading data or waiting for a delay.

  • "This function can be awaited."
    Other code calling this function must pause and wait until it finishes—without blocking the main thread.

Swift Code Example:

struct Weather {
    let temperature: Double
    let condition: String
}

func fetchWeatherData() async -> Weather {
    try? await Task.sleep(nanoseconds: 2_000_000_000)
    return Weather(temperature: 25.0, condition: "Sunny")
}

How to Write Your Own “async” Functions

Basic Structure

func yourFunctionName() async -> ReturnType {

}

Example: Simulating a Data Fetch

func fetchUserName() async -> String {
    try? await Task.sleep(nanoseconds: 2_000_000_000) // simulate delay
    return "Nusrat"
}

Calling Your Async Function

You must call it using await, and only from another async context:

func displayUser() async {
    let name = await fetchUserName()
    print("Hello, \(name)!")
}

By writing your own async functions, you gain full control over how and when tasks are executed — keeping your app smooth, non-blocking, and modern.

How to Handle Errors When Working with Async Code

Sometimes, asynchronous functions can fail — for example, a network request might time out, or a file might not be found. Swift lets you handle these failures safely using try, catch and the throws keyword.


When to Use ‘try’ and ‘throws’

To signal that a function can throw an error, you mark it with the ‘throws’ keyword:

func loadUserProfile() async throws -> String {
    // Simulate a possible error
    let success = Bool.random()
    if success {
        return "User: Nusrat"
    } else {
        throw URLError(.badServerResponse)
    }
}

Now that this function might fail, you must use try when calling it:

let name = try await loadUserProfile()

If an error occurs, Swift will throw it — so you also need to use a do-catch block to handle it safely.

Full Example: Using ‘try’, ‘await’, and ‘do-catch’

func showUserProfile() async {
    do {
        let name = try await loadUserProfile()
        print("Loaded: \(name)")
    } catch {
        print("Failed to load profile: \(error.localizedDescription)")
    }
}

Summary

  • Use throws to declare that a function might fail

  • Use try when calling a function that throws

  • Use do-catch to handle errors gracefully

This approach helps keep your async code both safe and predictable — without crashing your app.

Up next in Part 3: We’ll bring everything to life with real SwiftUI views, async calls, and smooth loading states.
Stay tuned — this is where theory meets practice!

0
Subscribe to my newsletter

Read articles from Tabassum Akter Nusrat directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tabassum Akter Nusrat
Tabassum Akter Nusrat

Former Android dev at Samsung R&D, now diving deep into iOS development. Writing daily about Swift Concurrency, SwiftUI, and the journey of building real-world apps. Passionate about clean architecture, mental health tech, and learning in public.