JavaScript Promise & async/await Guide | Async Patterns for Node & Browser

JavaScript Promise & async/await Guide | Async Patterns for Node & Browser

이 글의 핵심

Master Promises and async/await: readable asynchronous code, parallel and sequential flows, and robust error handling in modern JavaScript.

Introduction

What is asynchronous programming?

Asynchronous code does not block the rest of the program while waiting for a long-running operation.

Sync vs async:

console.log("1");
console.log("2");
console.log("3");

console.log("1");
setTimeout(() => console.log("2"), 1000);
console.log("3");

Why async matters:

  • Network: HTTP calls can take hundreds of ms or more
  • Disk: file I/O in Node.js
  • Timers: setTimeout, setInterval
  • User input: clicks, keyboard

1. Callbacks

Basic callback

function fetchData(callback) {
    setTimeout(() => {
        const data = { id: 1, name: "Alice" };
        callback(data);
    }, 1000);
}

console.log("start");
fetchData(data => {
    console.log("data:", data);
});
console.log("end");

Callback hell

getUser(userId, (user) => {
    getOrders(user.id, (orders) => {
        getOrderDetails(orders[0].id, (details) => {
            console.log(details);
        });
    });
});

2. Promises

What is a Promise?

A Promise represents the eventual outcome of an async operation.

States:

  • Pending: initial
  • Fulfilled: success
  • Rejected: failure

Creating a Promise

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = true;
        if (success) resolve("ok");
        else reject("fail");
    }, 1000);
});

promise
    .then(result => console.log(result))
    .catch(err => console.error(err))
    .finally(() => console.log("done"));

Chaining

function fetchUser(userId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id: userId, name: "Alice" });
        }, 1000);
    });
}

function fetchOrders(userId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve([{ id: 1, product: "laptop" }]);
        }, 1000);
    });
}

function fetchOrderDetails(orderId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id: orderId, price: 1200 });
        }, 1000);
    });
}

fetchUser(1)
    .then(user => {
        console.log("user:", user);
        return fetchOrders(user.id);
    })
    .then(orders => {
        console.log("orders:", orders);
        return fetchOrderDetails(orders[0].id);
    })
    .then(details => {
        console.log("details:", details);
    })
    .catch(err => console.error("error:", err))
    .finally(() => console.log("all steps finished"));

Rule: return the next Promise from then to continue the chain.

promise
    .then(result => {
        return anotherPromise();
    })
    .then(result2 => { });

promise
    .then(result => {
        anotherPromise();
    })
    .then(result2 => {
        // result2 may be undefined if you forgot return
    });

Static methods

Promise.resolve(42).then(console.log);

Promise.reject("err").catch(console.error);

Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
    .then(console.log);

Promise.all([
    Promise.resolve(1),
    Promise.reject("err"),
    Promise.resolve(3)
]).catch(console.error);

Promise.allSettled([
    Promise.resolve(1),
    Promise.reject("err"),
    Promise.resolve(3)
]).then(console.log);

Promise.race([
    new Promise(r => setTimeout(() => r(1), 1000)),
    new Promise(r => setTimeout(() => r(2), 500)),
]).then(console.log);

Promise.any([
    Promise.reject("e1"),
    new Promise(r => setTimeout(() => r(2), 500)),
]).then(console.log);

Practical snippets:

async function loadDashboard() {
    try {
        const [user, orders, notifications] = await Promise.all([
            fetchUser(),
            fetchOrders(),
            fetchNotifications()
        ]);
        renderDashboard(user, orders, notifications);
    } catch (error) {
        console.error("dashboard load failed:", error);
    }
}

function timeout(ms) {
    return new Promise((_, reject) =>
        setTimeout(() => reject(new Error("timeout")), ms)
    );
}

async function fetchWithTimeout(url, ms = 5000) {
    return Promise.race([fetch(url), timeout(ms)]);
}

3. async/await

Basics

async function fetchData() {
    return "data";
}

fetchData().then(console.log);

async function getData() {
    const data = await fetchData();
    console.log(data);
}

Sequential flow

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function fetchUser(userId) {
    await delay(1000);
    return { id: userId, name: "Alice" };
}

async function main() {
    try {
        const user = await fetchUser(1);
        const orders = await fetchOrders(user.id);
        const details = await fetchOrderDetails(orders[0].id);
        console.log(details);
    } catch (error) {
        console.error("error:", error);
    }
}

Parallelism

async function sequential() {
    const u1 = await fetchUser(1);
    const u2 = await fetchUser(2);
    return [u1, u2];
}

async function parallel() {
    const [u1, u2, u3] = await Promise.all([
        fetchUser(1),
        fetchUser(2),
        fetchUser(3)
    ]);
    return [u1, u2, u3];
}

async function parallel2() {
    const p1 = fetchUser(1);
    const p2 = fetchUser(2);
    return [await p1, await p2];
}

4. Error handling

try/catch with async/await

async function fetchData() {
    try {
        const response = await fetch("https://api.example.com/data");
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        console.error(error.message);
        return null;
    } finally {
        console.log("request finished");
    }
}

Promise errors

fetch("https://api.example.com/data")
    .then(response => {
        if (!response.ok) throw new Error("HTTP error");
        return response.json();
    })
    .then(console.log)
    .catch(console.error)
    .finally(() => console.log("done"));

5. Practical examples

Fetch GitHub user

async function fetchGitHubUser(username) {
    try {
        const response = await fetch(`https://api.github.com/users/${username}`);
        if (!response.ok) {
            throw new Error(`User not found: ${response.status}`);
        }
        const user = await response.json();
        return { name: user.name, bio: user.bio, repos: user.public_repos };
    } catch (error) {
        console.error(error.message);
        return null;
    }
}

Retry with backoff

async function fetchWithRetry(url, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await fetch(url);
            if (response.ok) return await response.json();
        } catch (error) {
            console.log(`attempt ${i + 1} failed`);
            if (i === maxRetries - 1) throw new Error("max retries exceeded");
            await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
        }
    }
}

Timeout wrapper

function timeout(ms) {
    return new Promise((_, reject) => {
        setTimeout(() => reject(new Error("Timeout")), ms);
    });
}

async function fetchWithTimeout(url, ms = 5000) {
    try {
        const response = await Promise.race([fetch(url), timeout(ms)]);
        return await response.json();
    } catch (error) {
        if (error.message === "Timeout") console.error("request timed out");
        throw error;
    }
}

Parallel fetch with partial failure

async function fetchMultipleUsers(userIds) {
    const promises = userIds.map(id => fetchUser(id));
    try {
        return await Promise.all(promises);
    } catch (error) {
        console.error("fetch failed:", error);
        return [];
    }
}

async function fetchMultipleUsersSettled(userIds) {
    const results = await Promise.allSettled(userIds.map(id => fetchUser(id)));
    return results
        .filter(r => r.status === "fulfilled")
        .map(r => r.value);
}

Sequential processing

async function processSequentially(items) {
    let result = 0;
    for (let item of items) {
        result = await processItem(item, result);
    }
    return result;
}

async function processSequentiallyReduce(items) {
    return items.reduce(async (accP, item) => {
        const acc = await accP;
        return processItem(item, acc);
    }, Promise.resolve(0));
}

6. Event loop

JavaScript runs one thread per realm, but the event loop schedules async work.

console.log("1");

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

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

console.log("4");

Rough order:

  1. Sync code: 1, 4
  2. Microtasks (Promises): 3
  3. Macrotasks (setTimeout): 2
setTimeout(() => console.log("macrotask"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("sync");

7. Patterns

Loading state

class DataFetcher {
    constructor() {
        this.loading = false;
        this.data = null;
        this.error = null;
    }

    async fetch(url) {
        this.loading = true;
        this.error = null;
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            this.data = await response.json();
        } catch (error) {
            this.error = error.message;
        } finally {
            this.loading = false;
        }
        return this.data;
    }
}

Simple cache

class CachedFetcher {
    constructor() {
        this.cache = new Map();
    }

    async fetch(url) {
        if (this.cache.has(url)) return this.cache.get(url);
        const response = await fetch(url);
        const data = await response.json();
        this.cache.set(url, data);
        return data;
    }
}

Concurrency queue

class TaskQueue {
    constructor(concurrency = 1) {
        this.concurrency = concurrency;
        this.running = 0;
        this.queue = [];
    }

    async add(task) {
        return new Promise((resolve, reject) => {
            this.queue.push({ task, resolve, reject });
            this.process();
        });
    }

    async process() {
        if (this.running >= this.concurrency || this.queue.length === 0) return;
        this.running++;
        const { task, resolve, reject } = this.queue.shift();
        try {
            resolve(await task());
        } catch (error) {
            reject(error);
        } finally {
            this.running--;
            this.process();
        }
    }
}

8. Common mistakes

Forgetting await

async function getData() {
    const data = fetchData();
    console.log(data);
}

async function getDataFixed() {
    const data = await fetchData();
    console.log(data);
}

Serial vs parallel

async function slow() {
    const u1 = await fetchUser(1);
    const u2 = await fetchUser(2);
    return [u1, u2];
}

async function fast() {
    return Promise.all([fetchUser(1), fetchUser(2)]);
}

forEach with await

async function bad(items) {
    items.forEach(async item => {
        await processItem(item);
    });
    console.log("done too early");
}

async function goodSequential(items) {
    for (const item of items) await processItem(item);
    console.log("done");
}

async function goodParallel(items) {
    await Promise.all(items.map(processItem));
    console.log("done");
}

Missing try/catch

async function getData() {
    try {
        return await fetchData();
    } catch (error) {
        console.error(error);
        return null;
    }
}

9. Exercises

Rewrite chain as async/await

async function getDataAsync() {
    try {
        const user = await fetchUser(1);
        const orders = await fetchOrders(user.id);
        return await fetchOrderDetails(orders[0].id);
    } catch (error) {
        console.error(error);
    }
}

Parallel users, sequential orders per user

async function processUsers(userIds) {
    const users = await Promise.all(userIds.map(id => fetchUser(id)));
    const results = [];
    for (const user of users) {
        const orders = await fetchOrders(user.id);
        results.push({ user, orders });
    }
    return results;
}

Promisify a callback API

function readFileCallback(filename, callback) {
    setTimeout(() => {
        callback(null, `contents of ${filename}`);
    }, 1000);
}

function readFilePromise(filename) {
    return new Promise((resolve, reject) => {
        readFileCallback(filename, (error, data) => {
            if (error) reject(error);
            else resolve(data);
        });
    });
}

Summary

Key points

  1. Styles: callbacks (basic), Promises (composition), async/await (readability).
  2. Static helpers: Promise.all, Promise.allSettled, Promise.race, Promise.any.
  3. Errors: .catch on chains; try/catch around await.
  4. Performance: sequential await vs Promise.all for independence.
  5. Event loop: microtasks before the next macrotask.

Best practices

  1. Prefer async/await for linear flow.
  2. Use Promise.all when tasks are independent.
  3. Always handle errors.
  4. Add timeouts for network calls when appropriate.
  5. Consider retries with backoff for flaky APIs.

Next steps

  • JavaScript DOM
  • JavaScript classes
  • JavaScript modules

  • Node.js async programming | callbacks, Promises, async/await
  • JavaScript functions | declarations, arrows, callbacks, closures