Skip to main content
โšก Calmops

Async/Await: Modern Asynchronous Programming

Async/Await: Modern Asynchronous Programming

Async/await provides a cleaner syntax for working with Promises, making asynchronous code look synchronous.

What is Async/Await?

Async/await is syntactic sugar built on top of Promises:

// Promise-based
function fetchUser(id) {
    return fetch(`/api/users/${id}`)
        .then(response => response.json())
        .then(user => user);
}

// Async/await
async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;
}

Async Functions

An async function always returns a Promise:

async function greet(name) {
    return `Hello, ${name}!`;
}

greet("Alice").then(message => console.log(message)); // "Hello, Alice!"

Implicit Promise Wrapping

async function test() {
    return 42;
}

// Equivalent to
function test() {
    return Promise.resolve(42);
}

test().then(value => console.log(value)); // 42

Await Expression

The await keyword pauses execution until a Promise settles:

async function example() {
    console.log("Start");
    
    const result = await Promise.resolve("Done");
    console.log(result); // "Done"
    
    console.log("End");
}

example();
// Output:
// Start
// Done
// End

Await with Delays

async function delayedGreeting() {
    console.log("Waiting...");
    
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    console.log("Hello after 2 seconds!");
}

delayedGreeting();

Error Handling

Try-Catch

async function fetchUser(id) {
    try {
        const response = await fetch(`/api/users/${id}`);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const user = await response.json();
        return user;
    } catch (error) {
        console.error("Error fetching user:", error);
        throw error;
    }
}

fetchUser(1)
    .then(user => console.log(user))
    .catch(error => console.error(error));

Multiple Try-Catch Blocks

async function processData() {
    try {
        const data = await fetchData();
        console.log("Data fetched");
    } catch (error) {
        console.error("Fetch error:", error);
    }
    
    try {
        const processed = await processData(data);
        console.log("Data processed");
    } catch (error) {
        console.error("Processing error:", error);
    }
}

Finally Block

async function operation() {
    try {
        const result = await someAsyncOperation();
        return result;
    } catch (error) {
        console.error(error);
    } finally {
        console.log("Cleanup"); // Always runs
    }
}

Sequential vs Parallel Execution

Sequential (Slower)

async function sequential() {
    const user = await fetchUser(1);
    const orders = await fetchOrders(user.id);
    const details = await fetchOrderDetails(orders[0].id);
    
    return details;
}

// Takes: time1 + time2 + time3

Parallel (Faster)

async function parallel() {
    const [user, posts, comments] = await Promise.all([
        fetchUser(1),
        fetchPosts(1),
        fetchComments(1)
    ]);
    
    return { user, posts, comments };
}

// Takes: max(time1, time2, time3)

Practical Examples

Fetch with Retry

async function fetchWithRetry(url, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            return await response.json();
        } catch (error) {
            if (i === maxRetries - 1) throw error;
            console.log(`Retry ${i + 1}...`);
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
    }
}

fetchWithRetry("https://api.example.com/data")
    .then(data => console.log(data))
    .catch(error => console.error(error));

Timeout

async function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error("Timeout")), ms)
    );
    
    return Promise.race([promise, timeout]);
}

async function fetchWithTimeout() {
    try {
        const data = await withTimeout(
            fetch("https://api.example.com/data").then(r => r.json()),
            5000
        );
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

Sequential Processing

async function processItems(items) {
    const results = [];
    
    for (const item of items) {
        const result = await processItem(item);
        results.push(result);
    }
    
    return results;
}

async function processItem(item) {
    return new Promise(resolve => {
        setTimeout(() => resolve(`Processed: ${item}`), 1000);
    });
}

processItems(["A", "B", "C"])
    .then(results => console.log(results));

Parallel Processing

async function processItemsParallel(items) {
    const promises = items.map(item => processItem(item));
    return Promise.all(promises);
}

processItemsParallel(["A", "B", "C"])
    .then(results => console.log(results));

Data Transformation Pipeline

async function getUserWithDetails(userId) {
    try {
        // Fetch user
        const userResponse = await fetch(`/api/users/${userId}`);
        const user = await userResponse.json();
        
        // Fetch user's posts
        const postsResponse = await fetch(`/api/users/${userId}/posts`);
        const posts = await postsResponse.json();
        
        // Fetch user's comments
        const commentsResponse = await fetch(`/api/users/${userId}/comments`);
        const comments = await commentsResponse.json();
        
        return {
            user,
            posts,
            comments,
            postCount: posts.length,
            commentCount: comments.length
        };
    } catch (error) {
        console.error("Error fetching user details:", error);
        throw error;
    }
}

getUserWithDetails(1)
    .then(details => console.log(details))
    .catch(error => console.error(error));

Event Handler with Async

const button = document.getElementById("myButton");

button.addEventListener("click", async (event) => {
    try {
        button.disabled = true;
        button.textContent = "Loading...";
        
        const data = await fetch("/api/data").then(r => r.json());
        console.log(data);
        
        button.textContent = "Success!";
    } catch (error) {
        console.error(error);
        button.textContent = "Error";
    } finally {
        button.disabled = false;
    }
});

Async Arrow Functions

// Regular async function
async function fetchData() {
    return await fetch("/api/data").then(r => r.json());
}

// Async arrow function
const fetchData = async () => {
    return await fetch("/api/data").then(r => r.json());
};

// Concise
const fetchData = async () => fetch("/api/data").then(r => r.json());

Async Generators

async function* asyncGenerator() {
    yield 1;
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield 2;
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield 3;
}

(async () => {
    for await (const value of asyncGenerator()) {
        console.log(value);
    }
})();

Common Patterns

Debounce with Async

function debounceAsync(fn, delay) {
    let timeoutId;
    
    return async (...args) => {
        clearTimeout(timeoutId);
        return new Promise(resolve => {
            timeoutId = setTimeout(() => {
                resolve(fn(...args));
            }, delay);
        });
    };
}

const search = debounceAsync(async (query) => {
    const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
    return results;
}, 300);

Memoization with Async

function memoizeAsync(fn) {
    const cache = new Map();
    
    return async (...args) => {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            return cache.get(key);
        }
        
        const result = await fn(...args);
        cache.set(key, result);
        return result;
    };
}

const fetchUser = memoizeAsync(async (id) => {
    return fetch(`/api/users/${id}`).then(r => r.json());
});

Best Practices

Use Parallel When Possible

// Bad - sequential
const user = await fetchUser(1);
const posts = await fetchPosts(1);

// Good - parallel
const [user, posts] = await Promise.all([
    fetchUser(1),
    fetchPosts(1)
]);

Handle Errors Properly

// Good - specific error handling
async function operation() {
    try {
        const result = await riskyOperation();
        return result;
    } catch (error) {
        if (error instanceof NetworkError) {
            console.error("Network error:", error);
        } else if (error instanceof ValidationError) {
            console.error("Validation error:", error);
        } else {
            console.error("Unknown error:", error);
        }
        throw error;
    }
}

Avoid Unnecessary Await

// Bad - unnecessary await
async function bad() {
    const result = await Promise.resolve(42);
    return result;
}

// Good - return promise directly
async function good() {
    return Promise.resolve(42);
}

Summary

  • async: declares async function that returns Promise
  • await: pauses execution until Promise settles
  • try-catch: error handling in async functions
  • Sequential: await operations one after another
  • Parallel: use Promise.all() for concurrent operations
  • Error handling: use try-catch-finally
  • Best practice: use parallel execution when possible

Official Documentation

Next Steps

  1. Fetch API: Making HTTP Requests
  2. Error Handling with Promises and Async/Await
  3. DOM: Document Object Model Basics

Comments