C
JavaScript/Async/Lesson 17

async / await — *Async Like Sync*

45 min·theory
This chapter
3/3

async / await — *Async Like Sync*

🎯 After Reading This Lesson

By the end of this lesson, you will be able to confidently do the following 3 things.

  • ✅ Why async / await is syntactic sugar over Promise
  • ✅ Why await inside forEach doesn't work + fix with for-of
  • ✅ Implementing a fetch timeout with AbortController

Keep the learning goals as a checklist — close the lesson only when you can answer all of them.

async / await

The Core in One Line

async/await (ES2017) = syntax for writing Promises like synchronous code. Far more readable than then-chaining. The standard in modern JS.

Comparison

javascript
// 🧪 Fake API — Responds after 50ms (mock for copy-pasting)
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const fetchUser   = (id) => wait(50).then(() => ({ id, name: 'Hong' }));
const fetchOrders = (uid) => wait(50).then(() => [{ orderId: 1, userId: uid }]);

// 👴 Old way — then chaining (nested indentation)
function getUserDataOld(id) {
    return fetchUser(id)
        .then(user => {
            return fetchOrders(user.id)        // ← Can only be called if user exists
                .then(orders => ({ user, orders }));
        });
}

// 🆕 Modern way — async / await (flat like synchronous code)
async function getUserData(id) {
    const user   = await fetchUser(id);        // ① Wait until user is received
    const orders = await fetchOrders(user.id); // ② Then receive orders
    return { user, orders };
}

// ▶️ Call — Both methods yield the same result (IIFE to avoid top-level await)
(async () => {
    const data = await getUserData(42);
    console.log(data);   // { user: { id: 42, name: 'Hong' }, orders: [{ orderId: 1, userId: 42 }] }
})();

Almost identical in shape to synchronous code. Reads linearly.

Two Rules

1. await only inside an async function:

javascript
async function fn() {
    const x = await promise;   // ✅
}

const x = await promise;       // ❌ Only in some environments at the Top-level

2. An async function always returns a Promise:

javascript
async function fn() {
    return 42;       // Returns a plain number, but...
}

const result = fn();           // Promise { 42 }   ← "Wrapped" in a Promise!
console.log(result);           // Promise { 42 }

const v = await fn();          // 42   ← "Unwrapped" with await to get the real value
console.log(v);                // 42

// 💡 All return X from an async function are wrapped in a Promise
//    → The receiver must unwrap it with .then() or await

Error Handling — try-catch

javascript
async function getUserData(id) {
    try {
        const user = await fetchUser(id);
        const orders = await fetchOrders(user.id);
        return { user, orders };
    } catch (err) {
        console.error("Failed:", err);
        throw err;          // Propagate upwards
    }
}

Use the familiar try-catch instead of then-catch. Exception flow is identical to regular code.

Parallelism — The Sequential await Trap

javascript
// 🧪 Fake API — Assumes 100ms (simulated with setTimeout instead of actual fetch)
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const fetchUser    = (id) => wait(100).then(() => ({ id, name: 'A' }));
const fetchOrders  = (id) => wait(100).then(() => [{ orderId: 1 }]);
const fetchProfile = (id) => wait(100).then(() => ({ bio: 'Developer' }));

// ❌ Sequential — Next line starts only after the previous one finishes (slow)
async function bad() {
    console.time('bad');
    const user    = await fetchUser(42);      // ⏱️ 0 → 100ms (wait)
    const orders  = await fetchOrders(42);    // ⏱️ 100 → 200ms
    const profile = await fetchProfile(42);   // ⏱️ 200 → 300ms
    console.timeEnd('bad');                   // bad: ~300ms
}

// ✅ Parallel — All three requests start "simultaneously" (fast)
async function good() {
    console.time('good');
    const [user, orders, profile] = await Promise.all([
        fetchUser(42),      // ← Start simultaneously
        fetchOrders(42),    // ← Start simultaneously
        fetchProfile(42)    // ← Start simultaneously
    ]);
    console.timeEnd('good');                  // good: ~100ms (only as long as the slowest one)
}

// ▶️ Execute (Wrapped in IIFE to avoid top-level await → OK to copy-paste anywhere)
(async () => {
    await bad();    // 📤 bad: ~300ms
    await good();   // 📤 good: ~100ms   ← 3 times faster!
})();

// 💡 Linear await: "Next starts after previous finishes" → Waits sequentially even without dependencies
//    Promise.all : "All start simultaneously" → Takes only as long as the slowest one

When requests are independent of each other, always parallelize.

Real-World Pattern — Retry

javascript
async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const res = await fetch(url);
            if (res.ok) return res.json();
            throw new Error(`HTTP ${res.status}`);
        } catch (err) {
            if (i === retries - 1) throw err;
            await new Promise(r => setTimeout(r, 1000 * (i + 1)));   // 1·2·3 seconds
        }
    }
}

Top-level await — In Modules

Since ES2022, await is available at the top level of modules:

javascript
// module.mjs
const config = await fetch('/config.json').then(r => r.json());
export { config };

Supported in Node.js and modern browsers (modules). Keeps entry points clean.

In React Components

jsx
function UserProfile({ id }) {
    const [user, setUser] = useState(null);

    useEffect(() => {
        // useEffect callback cannot be async (should not return a Promise)
        // → Define and call an async function inside
        async function load() {
            const data = await fetch(`/api/users/${id}`).then(r => r.json());
            setUser(data);
        }
        load();
    }, [id]);

    if (!user) return <p>Loading...</p>;
    return <h1>{user.name}</h1>;
}

Quick Summary

  • async function + await Promise = async that feels synchronous
  • Errors handled with try-catch
  • Independent requests go parallel with Promise.all
  • The modern JS standard — then-chaining is rarely used anymore

⚡ Try It Yourself — async / await + Sequential vs. Parallel

Compare timings to see why sequential await is slow and why Promise.all is fast.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

🤖 Try Asking AI Like This

Knowing the concepts from this lesson lets you give AI specific instructions. Not a vague 'fix this' but a request with vocabulary — that's where token savings begin.

  • 'This await inside forEach isn't working — convert it to for...of or Promise.all'
  • 'Add an AbortController timeout to this fetch'
  • 'Add try-catch with a friendly error message to this function'

Why This Saves Tokens

Without understanding the concepts, you have to ask 'What does that mean?' after every AI response. Those follow-up questions eat up tokens. Learn the concept once and the conversation wraps up in a single exchange.

Async/Await - JavaScript