Skip to content

JavaScript Async Programming

Asynchronous programming is one of the core features of JavaScript. It allows programs to continue executing other code while waiting for time-consuming operations (such as network requests, file I/O, timers, etc.) to complete, improving responsiveness and performance. Understanding asynchronous programming is crucial for building modern web applications.

What is Async Programming

Asynchronous programming means that when a program executes time-consuming operations, it doesn't block subsequent code execution. Instead, it continues executing other tasks and handles results through callbacks, Promises, or async/await when the operation completes.

Synchronous vs Asynchronous

javascript
// Synchronous code example
console.log("1. Start execution");

// This blocks subsequent code (not recommended in practice)
function syncDelay(ms) {
    const start = Date.now();
    while (Date.now() - start < ms) {
        // Empty loop, blocks execution
    }
}

syncDelay(2000); // Blocks for 2 seconds
console.log("2. Sync delay ended");

// Asynchronous code example
console.log("1. Start execution");

setTimeout(() => {
    console.log("3. Async operation completed");
}, 2000);

console.log("2. Continue executing other code");

Callback Functions

Callbacks were the earliest async programming approach, but can lead to callback hell.

Basic Callback

javascript
// Simple callback example
function fetchData(callback) {
    setTimeout(() => {
        const data = { id: 1, name: "John" };
        callback(null, data); // First param is error, second is data
    }, 1000);
}

fetchData((error, data) => {
    if (error) {
        console.error("Failed to fetch data:", error);
    } else {
        console.log("Data received:", data);
    }
});

Callback Hell

javascript
// Callback hell example
step1((error1, result1) => {
    if (error1) {
        console.error("Step 1 failed:", error1);
        return;
    }
    
    step2(result1, (error2, result2) => {
        if (error2) {
            console.error("Step 2 failed:", error2);
            return;
        }
        
        step3(result2, (error3, result3) => {
            if (error3) {
                console.error("Step 3 failed:", error3);
                return;
            }
            
            console.log("All steps completed:", result3);
        });
    });
});

Promise

Promise was introduced in ES6 as an async programming solution, solving the callback hell problem.

Promise Basics

javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
            resolve("Operation successful!");
        } else {
            reject(new Error("Operation failed!"));
        }
    }, 1000);
});

// Using Promise
myPromise
    .then(result => {
        console.log("Success:", result);
    })
    .catch(error => {
        console.error("Failure:", error.message);
    })
    .finally(() => {
        console.log("Operation completed (regardless of outcome)");
    });

Promise States

javascript
// Three Promise states
// pending - Still executing
const pendingPromise = new Promise(() => {});

// fulfilled - Successfully resolved
const fulfilledPromise = Promise.resolve("Success");

// rejected - Failed
const rejectedPromise = Promise.reject(new Error("Failed"));

Promise Chaining

javascript
// Promise chain
fetchUserData(1)
    .then(user => {
        console.log("Got user:", user);
        return fetchUserPosts(user.id);
    })
    .then(posts => {
        console.log("Got posts:", posts);
        return posts.length;
    })
    .then(count => {
        console.log("Post count:", count);
    })
    .catch(error => {
        console.error("Operation failed:", error.message);
    });

Promise Static Methods

javascript
// Promise.all() - All must succeed
Promise.all([
    Promise.resolve(1),
    Promise.resolve(2),
    Promise.resolve(3)
]).then(results => {
    console.log("All Promises completed:", results); // [1, 2, 3]
});

// Promise.allSettled() - Wait for all to complete (ES2020)
Promise.allSettled([
    Promise.resolve(1),
    Promise.reject(new Error("Failed")),
    Promise.resolve(3)
]).then(results => {
    console.log("All settled:", results);
});

// Promise.race() - First to complete wins
const fast = new Promise(resolve => setTimeout(() => resolve("Fast"), 100));
const slow = new Promise(resolve => setTimeout(() => resolve("Slow"), 1000));

Promise.race([fast, slow])
    .then(result => console.log("Winner:", result)); // "Fast"

// Promise.any() - First success wins (ES2021)
Promise.any([fast, slow])
    .then(result => console.log("First success:", result));

async/await

async/await was introduced in ES2017 as syntactic sugar, making async code look like sync code.

Basic Syntax

javascript
// async function
async function fetchData() {
    try {
        const response = await fetch("https://api.example.com/data");
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Failed to fetch data:", error);
        throw error;
    }
}

// Calling async function
fetchData()
    .then(data => console.log("Data:", data))
    .catch(error => console.error("Error:", error));

Error Handling

javascript
// async/await error handling
async function processUserData() {
    try {
        console.log("Fetching user data");
        const user = await fetchUserData(1);
        console.log("User:", user);
        
        console.log("Fetching user posts");
        const posts = await fetchUserPosts(user.id);
        console.log("Posts:", posts);
        
        return { user, posts };
    } catch (error) {
        console.error("Error processing user data:", error.message);
        throw new Error("User data processing failed: " + error.message);
    }
}

Parallel Execution

javascript
// Sequential execution (slower)
async function serialExecution() {
    const user = await fetchUserData(1);
    const posts = await fetchUserPosts(1);
    const comments = await fetchUserComments(1);
    return { user, posts, comments };
}

// Parallel execution (faster)
async function parallelExecution() {
    const [user, posts, comments] = await Promise.all([
        fetchUserData(1),
        fetchUserPosts(1),
        fetchUserComments(1)
    ]);
    return { user, posts, comments };
}

// Mixed execution
async function mixedExecution() {
    // First get user data
    const user = await fetchUserData(1);
    
    // Then fetch posts and comments in parallel
    const [posts, comments] = await Promise.all([
        fetchUserPosts(user.id),
        fetchUserComments(user.id)
    ]);
    
    return { user, posts, comments };
}

Async Iterators and Generators

Async Generator

javascript
// Async generator function
async function* asyncNumberGenerator() {
    let i = 0;
    while (i < 5) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        yield i++;
    }
}

// Using async generator
async function useAsyncGenerator() {
    for await (const number of asyncNumberGenerator()) {
        console.log("Generated number:", number);
    }
}

Microtasks and Macrotasks

Understanding JavaScript's event loop is important for async programming.

javascript
// Event loop example
console.log("1. Synchronous code");

setTimeout(() => {
    console.log("2. Macrotask (setTimeout)");
}, 0);

Promise.resolve().then(() => {
    console.log("3. Microtask (Promise)");
});

console.log("4. Synchronous code ends");

// Output order:
// 1. Synchronous code
// 4. Synchronous code ends
// 3. Microtask (Promise)
// 2. Macrotask (setTimeout)

Best Practices

1. Error Handling

javascript
// Unified error handling
class AsyncErrorHandler {
    static async handle(asyncFunction, ...args) {
        try {
            const result = await asyncFunction(...args);
            return { success: true, data: result };
        } catch (error) {
            console.error("Async operation failed:", error);
            return { success: false, error: error.message };
        }
    }
    
    static async retry(asyncFunction, maxRetries = 3, delay = 1000) {
        let lastError;
        
        for (let i = 0; i < maxRetries; i++) {
            try {
                return await asyncFunction();
            } catch (error) {
                lastError = error;
                console.log(`Attempt ${i + 1} failed: ${error.message}`);
                
                if (i < maxRetries - 1) {
                    await this.delay(delay * (i + 1));
                }
            }
        }
        
        throw new Error(`Failed after ${maxRetries} retries: ${lastError.message}`);
    }
    
    static delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

2. Timeout Control

javascript
// Promise with timeout
class PromiseWithTimeout {
    static async timeout(promise, ms) {
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
        });
        
        return Promise.race([promise, timeoutPromise]);
    }
}

// Usage
async function slowOperation() {
    await new Promise(resolve => setTimeout(resolve, 3000));
    return "Slow operation completed";
}

PromiseWithTimeout.timeout(slowOperation(), 2000)
    .then(result => console.log("Result:", result))
    .catch(error => console.error("Error:", error.message));

3. Concurrency Control

javascript
// Limit concurrent requests
class ConcurrencyController {
    constructor(limit = 3) {
        this.limit = limit;
        this.running = 0;
        this.queue = [];
    }
    
    async add(asyncFunction) {
        return new Promise((resolve, reject) => {
            this.queue.push({ asyncFunction, resolve, reject });
            this.process();
        });
    }
    
    async process() {
        if (this.running >= this.limit || this.queue.length === 0) {
            return;
        }
        
        this.running++;
        const { asyncFunction, resolve, reject } = this.queue.shift();
        
        try {
            const result = await asyncFunction();
            resolve(result);
        } catch (error) {
            reject(error);
        } finally {
            this.running--;
            this.process();
        }
    }
}

Practical Example: API Client

javascript
class ApiClient {
    constructor(baseURL, options = {}) {
        this.baseURL = baseURL;
        this.defaultOptions = {
            timeout: 5000,
            retries: 3,
            ...options
        };
    }
    
    async request(endpoint, options = {}) {
        const url = `${this.baseURL}${endpoint}`;
        const config = { ...this.defaultOptions, ...options };
        
        let lastError;
        
        for (let i = 0; i < config.retries; i++) {
            try {
                const response = await this.fetchWithTimeout(url, config);
                return await this.handleResponse(response);
            } catch (error) {
                lastError = error;
                console.log(`Attempt ${i + 1} failed: ${error.message}`);
                
                if (i < config.retries - 1) {
                    await this.delay(1000 * (i + 1));
                }
            }
        }
        
        throw new Error(`Request failed after ${config.retries} retries: ${lastError.message}`);
    }
    
    async fetchWithTimeout(url, options) {
        const { timeout, ...fetchOptions } = options;
        
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);
        
        try {
            const response = await fetch(url, {
                ...fetchOptions,
                signal: controller.signal
            });
            return response;
        } finally {
            clearTimeout(timeoutId);
        }
    }
    
    async handleResponse(response) {
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const contentType = response.headers.get("content-type");
        if (contentType && contentType.includes("application/json")) {
            return await response.json();
        }
        
        return await response.text();
    }
    
    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    // Convenience methods
    async get(endpoint, options = {}) {
        return this.request(endpoint, { method: "GET", ...options });
    }
    
    async post(endpoint, data, options = {}) {
        return this.request(endpoint, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data),
            ...options
        });
    }
    
    async put(endpoint, data, options = {}) {
        return this.request(endpoint, {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data),
            ...options
        });
    }
    
    async delete(endpoint, options = {}) {
        return this.request(endpoint, { method: "DELETE", ...options });
    }
}

// Usage example
const api = new ApiClient("https://jsonplaceholder.typicode.com");

// api.get("/users/1")
//     .then(user => console.log("User:", user))
//     .catch(error => console.error("Failed:", error));

Summary

Key points about JavaScript async programming:

  1. Basic Concept: Async programming allows programs to continue while waiting for operations
  2. Callbacks: Earliest async method, but can lead to callback hell
  3. Promise: ES6 solution supporting chaining and error handling
  4. async/await: ES2017 syntactic sugar making async code look synchronous
  5. Promise Static Methods: all(), race(), allSettled(), any()
  6. Async Iteration: Async generators and iterators
  7. Event Loop: Understanding microtasks and macrotasks execution order
  8. Best Practices: Error handling, timeout control, concurrency control
  9. Practical Applications: API clients, data loaders

Mastering async programming is a key skill for building modern JavaScript applications. In the next chapter, we will learn about JavaScript form handling.

Content is for learning and research only.