본문으로 건너뛰기
Previous
Next
JavaScript Promise & async/await Complete Guide

JavaScript Promise & async/await Complete Guide

JavaScript Promise & async/await Complete Guide

이 글의 핵심

JavaScript Promise and async/await tutorial: callbacks vs Promises, Promise.all/race/allSettled, error handling, and the event loop—patterns for Node.js and frontend apps.

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



자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. JavaScript Promise and async/await tutorial: callbacks vs Promises, Promise.all/race/allSettled, error handling, and the… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

JavaScript, Promise, async, await, Event Loop, Node.js 등으로 검색하시면 이 글이 도움이 됩니다.