Isaac.

Using Async and Await

Learn how to write asynchronous code using async/await in modern programming languages.

By EMEPublished: February 20, 2025
asyncawaitasynchronousjavascriptcsharppromisesconcurrency

A Simple Analogy

Imagine ordering coffee at a busy café. Instead of watching the barista make your coffee while blocking everyone behind you, they take your order and give you a number. You sit down, chat with friends, or check your phone while they make it. When your number is called, you pick it up. Async/await is like that—your code doesn't block waiting; it moves on to other work and checks back when the task is done.


What Are Async and Await?

async and await are keywords that allow you to write asynchronous code in a way that looks synchronous. They make it easy to handle long-running operations (database queries, API calls, file reads) without blocking your application.

  • async: Declares that a function will handle asynchronous operations
  • await: Pauses execution until a Promise/Task completes, then continues

Why Use Async/Await?

  • Non-blocking: Your app can do other work while waiting (serving other requests, UI stays responsive)
  • Readable: Looks like synchronous code but runs asynchronously
  • Better UX: Apps respond quickly instead of freezing
  • Scalability: Handle thousands of concurrent requests with fewer resources
  • Error handling: Use try/catch just like synchronous code

How It Works (Simplified)

1. Call async function
2. Hit await keyword
3. Function pauses, returns control
4. Meanwhile, other code runs
5. When task completes, function resumes
6. Result is returned

Async/Await in JavaScript

Basic Example

// Without async/await (old style with .then())
function fetchUser(id) {
    return fetch(`/api/users/${id}`)
        .then(res => res.json())
        .then(data => console.log(data))
        .catch(error => console.error(error));
}

// With async/await (modern, cleaner)
async function fetchUser(id) {
    try {
        const response = await fetch(`/api/users/${id}`);
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

// Call it
await fetchUser(1);

Multiple Awaits (Sequence)

async function getOrderDetails(orderId) {
    try {
        const order = await fetchOrder(orderId);      // Wait for order
        const items = await fetchOrderItems(orderId);  // Then wait for items
        const user = await fetchUser(order.userId);    // Then wait for user
        return { order, items, user };
    } catch (error) {
        console.error(error);
    }
}

Parallel Execution (Faster)

async function getOrderDetails(orderId) {
    try {
        // Fetch all three at the same time
        const [order, items, user] = await Promise.all([
            fetchOrder(orderId),
            fetchOrderItems(orderId),
            fetchUser(order.userId),
        ]);
        return { order, items, user };
    } catch (error) {
        console.error(error);
    }
}

Async/Await in C# / .NET

Basic Example

// Async method
public async Task<User> GetUserAsync(int id)
{
    try
    {
        var response = await _httpClient.GetAsync($"/api/users/{id}");
        var json = await response.Content.ReadAsStringAsync();
        var user = JsonConvert.DeserializeObject<User>(json);
        return user;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
        return null;
    }
}

// Calling it
var user = await GetUserAsync(1);

In a Controller

[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
    var user = await _userService.GetUserAsync(id);
    if (user == null)
        return NotFound();
    return Ok(user);
}

Task vs. Task

// No return value
public async Task LogEventAsync(string message)
{
    await _logger.WriteAsync(message);
}

// With return value
public async Task<int> CountUsersAsync()
{
    return await _db.Users.CountAsync();
}

Async/Await in Python

import asyncio

async def fetch_user(user_id):
    """Async function"""
    await asyncio.sleep(1)  # Simulate API call
    return {"id": user_id, "name": "Alice"}

async def main():
    user = await fetch_user(1)
    print(user)

# Run it
asyncio.run(main())

Concurrent Tasks

async def main():
    # Run tasks in parallel
    results = await asyncio.gather(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3),
    )
    print(results)

asyncio.run(main())

Common Patterns

Sequential (Wait for Each)

async function process() {
    const result1 = await task1();
    const result2 = await task2(result1);  // Depends on result1
    const result3 = await task3(result2);
    return result3;
}

Parallel (All at Once)

async function process() {
    const [r1, r2, r3] = await Promise.all([
        task1(),
        task2(),
        task3(),
    ]);
    return [r1, r2, r3];
}

Race (First to Finish)

async function fetchWithTimeout() {
    const result = await Promise.race([
        fetchData(),
        timeout(5000),  // 5 second timeout
    ]);
    return result;
}

Real-World Use Cases

  • Web APIs: Wait for database query, then send response
  • File uploads: Upload multiple files in parallel
  • Microservices: Call multiple services, combine results
  • UI interactions: Fetch data without freezing buttons/scroll
  • Data processing: Handle multiple tasks concurrently

Best Practices

  • Always await async functions (don't call without await unless intentional)
  • Use try/catch for error handling
  • Use Promise.all() for parallel tasks
  • Avoid nested awaits when possible (refactor for clarity)
  • Don't create async functions unless they actually use await
  • Be careful with async in loops (use for...of not .forEach())
  • Remember: await only works inside async functions

Common Pitfalls

Forgetting Await (Returns Promise, Not Value)

// Wrong - user is a Promise, not the actual data
const user = getUserAsync(1);

// Correct
const user = await getUserAsync(1);

Sequential When You Need Parallel

// Slow (waits 3 seconds total)
const r1 = await fetch1();  // 1s
const r2 = await fetch2();  // 1s
const r3 = await fetch3();  // 1s

// Fast (waits 1 second total)
const [r1, r2, r3] = await Promise.all([
    fetch1(),
    fetch2(),
    fetch3(),
]);

Async in Loops

// Wrong (slow, sequential)
users.forEach(async user => {
    await processUser(user);  // Wait one at a time
});

// Better (parallel)
await Promise.all(users.map(user => processUser(user)));

Related Concepts to Explore

  • Promises and Promise chaining
  • Event loops and concurrency models
  • Callbacks and callback hell
  • Generators and iterators
  • Reactive programming (RxJS)
  • Coroutines and fibers
  • Thread safety and synchronization
  • Deadlocks and race conditions
  • Throttling and debouncing
  • Stream processing

Summary

Async/await transformed asynchronous programming from callback hell to readable, maintainable code. By understanding how to properly use async/await, sequence operations, run tasks in parallel, and handle errors, you can build responsive applications that scale—whether it's handling thousands of web requests or keeping your UI smooth while processing data.