C
JavaScript/非同期/Lesson 17

async / await — *非同期を同期のように*

45分·theory
このチャプター
3/3

async / await — *非同期を同期のように*

🎯 このlessonを読み終えたら

このlessonをすべて読み終えたら、以下の3つを自信を持って説明できるようになります。

  • ✅ async / await が Promise の文法糖衣である理由
  • ✅ forEach 内の await が動作しない理由 + for-of による解決策
  • ✅ AbortController を使った fetch タイムアウトの実装

学習目標をチェックリストとして手元に置き、すべて答えられるようになったらlessonを閉じてください。

async / await

核心を一言で

async/await (ES2017) = Promise を同期コードのように記述するための文法。then チェーンよりはるかに読みやすい。モダン JS の標準。

比較

javascript
// 🧪 偽API — 50ms後に応答 (コピペ可能にモック)
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const fetchUser   = (id) => wait(50).then(() => ({ id, name: 'ホン' }));
const fetchOrders = (uid) => wait(50).then(() => [{ orderId: 1, userId: uid }]);

// 👴 古い方式 — thenチェイン (ネストされたインデント)
function getUserDataOld(id) {
    return fetchUser(id)
        .then(user => {
            return fetchOrders(user.id)        // ← user がないと呼び出し不可
                .then(orders => ({ user, orders }));
        });
}

// 🆕 モダンな方式 — async / await (同期のようにフラット)
async function getUserData(id) {
    const user   = await fetchUser(id);        // ① user を受け取るまで待機
    const orders = await fetchOrders(user.id); // ② その後ordersを受け取る
    return { user, orders };
}

// ▶️ 呼び出し — 両方の方式で結果は同じ (IIFEでトップレベルawaitを回避)
(async () => {
    const data = await getUserData(42);

同期コードとほぼ同じ形直線的に読めます。

2つのルール

1. awaitasync 関数の中でのみ使用可能:

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

const x = await promise;       // ❌ トップレベルでは一部環境のみ

2. async 関数は必ず Promise を返す:

javascript
async function fn() {
    return 42;       // 普通の数値をリターンするが...
}
const result = fn();           // Promise { 42 }   ← Promise で「ラップされる」!
console.log(result);           // Promise { 42 }

const v = await fn();          // 42   ← await で「解決する」と本当の値
console.log(v);                // 42

// 💡 async function の return X はすべてPromiseでラップされる
//    → 受け取る側で .then() または await で解決する必要がある

エラー処理 — 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("失敗:", err);
        throw err;          // 上位へ伝播
    }
}

then-catch の代わりに使い慣れた try-catch を使用。例外の流れが通常のコードと同じ

並列処理 — 直列 await の落とし穴

javascript
// 🧪 偽API — 100msかかると仮定 (実際のfetchの代わりにsetTimeoutで模倣)
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: '開発者' }));

// ❌ 直列 — 1行終わってから次の行が始まる (遅い)
async function bad() {
    console.time('bad');
    const user    = await fetchUser(42);      // ⏱️ 0 → 100ms (待機)
    const orders  = await fetchOrders(42);    // ⏱️ 100 → 200ms
    const profile = await fetchProfile(42);   // ⏱️ 200 → 300ms
    console.timeEnd('bad');                   // bad: ~300ms
}

// ✅ 並列 — 3つのリクエストが「同時」に開始 (速い)
async function good() {
    console.time('good');
    const [user, orders, profile] = await Promise.all([
        fetchUser(42),      // ← 同時スタート
        fetchOrders(42),    // ← 同時スタート
        fetchProfile(42)    // ← 同時スタート
    ]);
    console.timeEnd('good');                  // good: ~100ms (最も遅いものと同じだけ)
}

// ▶️ 実行 (IIFEで囲みトップレベルawaitを回避 → どこでもコピペOK)
(async () => {
    await bad();    // 📤 bad: ~300ms
    await good();   // 📤 good: ~100ms   ← 3倍速い!
})();

// 💡 直線的await: 「前が終わってから次が始まる」 → 依存性がないのに順番に待つ
//    Promise.all : 「すべて同時スタート」 → 最も遅いものと同じだけかかる

リクエストが互いに独立しているなら、必ず並列化しましょう。

実践パターン — リトライ

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秒
        }
    }
}

トップレベル await — モジュールで

ES2022 以降、モジュールの最上位で await が使用可能:

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

Node.js・モダンブラウザ(モジュール)でサポート。エントリポイントをすっきりと書けます。

React コンポーネントで

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

    useEffect(() => {
        // useEffectコールバックはasync不可 (Promiseをリターンしてはいけない)
        // → 内部でasync関数を定義して呼び出す
        async function load() {
            const data = await fetch(`/api/users/${id}`).then(r => r.json());
            setUser(data);
        }
        load();
    }, [id]);

    if (!user) return <p>読み込み中...</p>;
    return <h1>{user.name}</h1>;
}

まとめ

  • async function + await Promise = 同期のような非同期
  • エラーは try-catch で処理
  • 独立したリクエストは Promise.all で並列化
  • モダン JS の標準 — then チェーンはほぼ使わない

⚡ 実際に試してみよう — async / await + 直列 vs 並列

直列 await がなぜ遅いのか、Promise.all がなぜ速いのかを時間比較で確認しましょう。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

🤖 AI にこう依頼してみてください

このlessonの概念を理解すれば、AI に具体的に指示を出せます。漠然とした「直してください」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「この forEach の中の await が動かないので、for...of または Promise.all に変えてください」
  • 「この fetch に AbortController のタイムアウトを追加してください」
  • 「この関数に try-catch と分かりやすいエラーメッセージを追加してください」

なぜこれがトークンを減らすのか

概念を知らないと、AI の回答を受け取っても「それは何ですか?」と再度質問しなければなりません。その「再質問」がトークンを消費します。概念を一度身につければ、会話が一度で完結します。

先に読むとよい概念: Promise
Async/Await - JavaScript