Skip to content

JavaScript Async Programming

Mattscreative edited this page Feb 21, 2026 · 1 revision

JavaScript Async Programming Guide

Table of Contents


Introduction

Asynchronous JavaScript is essential for building responsive web applications. This guide covers callbacks, promises, async/await, and the Fetch API - the core tools for handling asynchronous operations in JavaScript.


Understanding Asynchronous JavaScript

JavaScript is single-threaded, meaning it can only do one thing at a time. Asynchronous programming allows you to execute long-running operations without blocking the main thread.

// Synchronous code - executes in order
console.log("1. Start");
console.log("2. Processing");
console.log("3. End");

// Asynchronous code - doesn't wait
console.log("1. Start");
setTimeout(() => console.log("2. Timeout!"), 1000);
console.log("3. End");
// Output: 1, 3, 2 (order may vary)

Callbacks

Callbacks are functions passed as arguments to other functions, executed after an operation completes.

// Simple callback
function greet(name, callback) {
    const message = "Hello, " + name;
    callback(message);
}

greet("Alice", (message) => {
    console.log(message);  // "Hello, Alice"
});

// Callback with error handling
function fetchData(callback) {
    // Simulate async operation
    setTimeout(() => {
        const data = { name: "Alice", age: 25 };
        const error = null;
        callback(error, data);
    }, 1000);
}

fetchData((error, data) => {
    if (error) {
        console.error("Error:", error);
    } else {
        console.log("Data:", data);
    }
});

// Callback hell - avoid this!
getData((error, data) => {
    if (error) return handleError(error);
    processData(data, (error, result) => {
        if (error) return handleError(error);
        saveResult(result, (error, saved) => {
            if (error) return handleError(error);
            console.log("Complete!");
        });
    });
});

Promises

Promises provide a cleaner way to handle asynchronous operations compared to callbacks.

Creating Promises

// Create a new Promise
const myPromise = new Promise((resolve, reject) => {
    const success = true;
    
    if (success) {
        resolve("Operation successful!");
    } else {
        reject("Operation failed!");
    }
});

// Using the promise
myPromise
    .then((result) => console.log(result))
    .catch((error) => console.error(error));

// Promise with setTimeout
function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(() => resolve("Done!"), ms);
    });
}

delay(1000).then((result) => console.log(result));

Promise States

A promise can be in one of three states:

// pending - initial state, neither fulfilled nor rejected
const pendingPromise = new Promise((resolve, reject) => {});

// fulfilled - operation completed successfully
const fulfilledPromise = Promise.resolve("Success!");

// rejected - operation failed
const rejectedPromise = Promise.reject("Error!");

// Check state (can't directly check, but can handle)
const promise = new Promise((resolve, reject) => {
    setTimeout(() => Math.random() > 0.5 ? resolve("OK") : reject("Fail"), 1000);
});

promise
    .then((result) => console.log("Fulfilled:", result))
    .catch((error) => console.log("Rejected:", error))
    .finally(() => console.log("Promise settled"));

Promise Methods

// Promise.resolve() - create resolved promise
const resolved = Promise.resolve(42);

// Promise.reject() - create rejected promise
const rejected = Promise.reject("Error");

// Promise.all() - wait for all promises
const results = Promise.all([
    Promise.resolve(1),
    Promise.resolve(2),
    Promise.resolve(3)
]);
results.then((values) => console.log(values));  // [1, 2, 3]

// Promise.race() - return first settled promise
const first = Promise.race([
    new Promise(r => setTimeout(() => r("fast"), 100)),
    new Promise(r => setTimeout(() => r("slow"), 200))
]);
first.then(console.log);  // "fast"

// Promise.allSettled() - wait for all to settle
const allSettled = Promise.allSettled([
    Promise.resolve(1),
    Promise.reject("error"),
    Promise.resolve(3)
]);
allSettled.then(console.log);
// [{status: "fulfilled", value: 1}, {status: "rejected", reason: "error"}, ...]

// Promise.any() - first fulfilled (ES2021)
const any = Promise.any([
    Promise.reject("error"),
    Promise.resolve("success"),
    Promise.resolve("also success")
]);
any.then(console.log);  // "success"

Promise Chaining

// Chain promises
fetchUser(1)
    .then((user) => fetchPosts(user.id))
    .then((posts) => fetchComments(posts[0].id))
    .then((comments) => console.log(comments))
    .catch((error) => console.error("Error:", error));

// Return values in chain
Promise.resolve(1)
    .then((x) => x + 1)    // 2
    .then((x) => x * 2)    // 4
    .then((x) => x.toString())  // "4"
    .then(console.log);

// Returning promises in chain
function fetchUser(id) {
    return fetch(`/api/users/${id}`).then(r => r.json());
}

fetchUser(1)
    .then((user) => {
        // Can return another promise here
        return fetch(`/api/posts?user=${user.id}`).then(r => r.json());
    })
    .then((posts) => console.log(posts));

Async/Await

Async/await provides a cleaner syntax for working with promises.

Basic Syntax

// async function declaration
async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
}

// arrow function
const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    return response.json();
};

// calling async function
fetchData()
    .then((data) => console.log(data))
    .catch((error) => console.error(error));

// or use IIFE
(async () => {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
})();

Error Handling

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

// Multiple try/catch blocks
async function getData() {
    try {
        const user = await fetchUser(1);
        
        try {
            const posts = await fetchPosts(user.id);
            return { user, posts };
        } catch (postError) {
            return { user, posts: [] };  // Graceful degradation
        }
    } catch (userError) {
        return { user: null, posts: [] };
    }
}

// Catch with async/await
async function main() {
    const user = await fetchUser(1).catch((error) => {
        console.error(error);
        return null;
    });
}

Parallel Execution

// Sequential (one after another)
async function sequential() {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return { user, posts, comments };
}

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

// When to use each:
// - Sequential: when each depends on previous result
// - Parallel: when operations are independent

Sequential vs Parallel

// Example: Fetching multiple users
const userIds = [1, 2, 3, 4, 5];

// Sequential - takes 5x time
async function fetchSequential() {
    const users = [];
    for (const id of userIds) {
        const user = await fetchUser(id);
        users.push(user);
    }
    return users;
}

// Parallel - takes 1x time
async function fetchParallel() {
    const promises = userIds.map(id => fetchUser(id));
    return Promise.all(promises);
}

// Mixed approach - fetch in batches
async function fetchInBatches(batchSize = 2) {
    const results = [];
    for (let i = 0; i < userIds.length; i += batchSize) {
        const batch = userIds.slice(i, i + batchSize);
        const batchResults = await Promise.all(batch.map(id => fetchUser(id)));
        results.push(...batchResults);
    }
    return results;
}

Fetch API

Basic Usage

// GET request
fetch('https://api.example.com/users')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.error(error));

// POST request
fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'Alice',
        email: 'alice@example.com'
    })
})
    .then(response => response.json())
    .then(data => console.log(data));

Request Options

// Complete fetch options
const options = {
    method: 'GET',           // GET, POST, PUT, DELETE, etc.
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token',
        'Accept': 'application/json'
    },
    body: JSON.stringify(data),  // for POST/PUT
    mode: 'cors',           // cors, same-origin, no-cors
    credentials: 'include',  // include, same-origin, omit
    cache: 'no-cache',      // default, no-cache, reload, force-cache
    redirect: 'follow',      // follow, error, manual
    referrer: 'no-referrer'
};

fetch(url, options);

Handling Responses

// Response methods
async function handleResponse(response) {
    // Check content type and parse accordingly
    const contentType = response.headers.get('content-type');
    
    if (contentType && contentType.includes('application/json')) {
        const data = await response.json();
        return data;
    }
    
    if (contentType && contentType.includes('text/html')) {
        const text = await response.text();
        return text;
    }
    
    // Handle other types...
    return response.blob();
}

// Response properties
async function inspectResponse(response) {
    console.log('Status:', response.status);        // 200
    console.log('Status Text:', response.statusText);  // OK
    console.log('OK:', response.ok);               // true if 200-299
    console.log('URL:', response.url);
    console.log('Type:', response.type);           // basic, cors, opaque
    
    // Headers
    for (const [key, value] of response.headers) {
        console.log(key, value);
    }
}

Advanced Patterns

Promise.all

Wait for all promises to resolve, fail if any fail:

// Fetch multiple resources
async function loadPage() {
    const [header, sidebar, content] = await Promise.all([
        fetch('/api/header').then(r => r.json()),
        fetch('/api/sidebar').then(r => r.json()),
        fetch('/api/content').then(r => r.json())
    ]);
    
    return { header, sidebar, content };
}

// Handle partial failures with allSettled first
async function loadWithPartialFailure() {
    const results = await Promise.allSettled([
        fetch('/api/data1').then(r => r.json()),
        fetch('/api/data2').then(r => r.json())
    ]);
    
    const data = results
        .filter(r => r.status === 'fulfilled')
        .map(r => r.value);
    
    return data;
}

Promise.race

Return the first promise to settle (resolve or reject):

// Timeout pattern
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('/api/slow'), 5000);
        return data;
    } catch (error) {
        console.error('Fetch failed or timed out');
    }
}

Promise.allSettled

Wait for all promises to settle (never rejects):

// Get results regardless of failures
async function fetchAllUsers(userIds) {
    const promises = userIds.map(id => 
        fetch(`/api/users/${id}`).then(r => r.json())
    );
    
    const results = await Promise.allSettled(promises);
    
    return results.map((result, index) => ({
        id: userIds[index],
        success: result.status === 'fulfilled',
        data: result.value,
        error: result.reason
    }));
}

Promise.any

Return first fulfilled promise (ignore rejections):

// Try multiple servers, use first success
async function fetchFromServers() {
    const urls = [
        'https://server1.example.com/data',
        'https://server2.example.com/data',
        'https://server3.example.com/data'
    ];
    
    const promises = urls.map(url => 
        fetch(url).then(r => r.json())
    );
    
    try {
        const data = await Promise.any(promises);
        return data;
    } catch (error) {
        console.error('All servers failed');
    }
}

Real-World Examples

API Calls

// REST API wrapper
class APIClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }
    
    async request(endpoint, options = {}) {
        const url = this.baseURL + endpoint;
        const config = {
            ...options,
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            }
        };
        
        try {
            const response = await fetch(url, config);
            
            if (!response.ok) {
                throw new APIError(response.status, await response.text());
            }
            
            return await response.json();
        } catch (error) {
            if (error instanceof APIError) throw error;
            throw new NetworkError(error.message);
        }
    }
    
    get(endpoint) {
        return this.request(endpoint);
    }
    
    post(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
        });
    }
    
    put(endpoint, data) {
        return this.request(endpoint, {
            method: 'PUT',
            body: JSON.stringify(data)
        });
    }
    
    delete(endpoint) {
        return this.request(endpoint, { method: 'DELETE' });
    }
}

class APIError extends Error {
    constructor(status, message) {
        super(message);
        this.status = status;
        this.name = 'APIError';
    }
}

class NetworkError extends Error {
    constructor(message) {
        super(message);
        this.name = 'NetworkError';
    }
}

// Usage
const api = new APIClient('https://api.example.com');
const users = await api.get('/users');
await api.post('/users', { name: 'Alice' });

File Operations (Node.js)

const fs = require('fs').promises;

// Read file
async function readFile(path) {
    try {
        const data = await fs.readFile(path, 'utf-8');
        return data;
    } catch (error) {
        console.error('Error reading file:', error);
        throw error;
    }
}

// Write file
async function writeFile(path, data) {
    await fs.writeFile(path, JSON.stringify(data, null, 2));
}

// Copy file
async function copyFile(source, destination) {
    await fs.copyFile(source, destination);
}

// Read directory
async function listFiles(path) {
    const entries = await fs.readdir(path, { withFileTypes: true });
    return entries.map(entry => ({
        name: entry.name,
        isDirectory: entry.isDirectory(),
        isFile: entry.isFile()
    }));
}

Event Handling

// Wait for event once
function once(element, eventName) {
    return new Promise((resolve) => {
        element.addEventListener(eventName, resolve, { once: true });
    });
}

// Usage
const button = document.getElementById('myButton');
const clickEvent = await once(button, 'click');
console.log('Button clicked at', clickEvent.timestamp);

// Wait for multiple events
function waitForEvents(emitter, events) {
    return new Promise((resolve) => {
        const results = {};
        let remaining = events.length;
        
        events.forEach(event => {
            emitter.on(event, (data) => {
                results[event] = data;
                remaining--;
                if (remaining === 0) resolve(results);
            });
        });
    });
}

Best Practices

// Always handle errors
async function good() {
    try {
        const data = await fetchData();
        return data;
    } catch (error) {
        console.error(error);
        throw error;
    }
}

// Use async/await over .then() chains
// Good
async function goodExample() {
    const user = await fetchUser(id);
    const posts = await fetchPosts(user.id);
    return posts;
}

// Avoid
function badExample() {
    return fetchUser(id)
        .then(user => fetchPosts(user.id));
}

// Use Promise.all for parallel operations
async function loadData() {
    const [users, posts] = await Promise.all([
        fetchUsers(),
        fetchPosts()
    ]);
    return { users, posts };
}

// Avoid blocking the main thread
// Use Web Workers for heavy computation
const worker = new Worker('worker.js');
worker.postMessage({ data: 'heavy' });
worker.onmessage = (event) => console.log(result);

// Cancel async operations with AbortController
const controller = new AbortController();
const signal = controller.signal;

async function fetchWithCancel() {
    try {
        const response = await fetch('/api/data', { signal });
        return await response.json();
    } catch (error) {
        if (error.name === 'AbortError') {
            console.log('Fetch aborted');
        } else {
            throw error;
        }
    }
}

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

Quick Reference

// Promise basics
const promise = new Promise((resolve, reject) => {});
promise.then(onFulfilled).catch(onRejected);

// Async/await
async function fetchData() {
    const data = await promise;
    return data;
}

// Fetch API
fetch(url, options)
    .then(r => r.json())
    .then(data => console.log(data));

// Promise.all - wait for all
const results = await Promise.all([p1, p2, p3]);

// Promise.race - first to settle
const result = await Promise.race([p1, p2]);

// Promise.allSettled - all settle
const results = await Promise.allSettled([p1, p2]);

// Error handling
try {
    const data = await fetchData();
} catch (error) {
    console.error(error);
}

Clone this wiki locally