コンテンツにスキップ

8-4. 非同期処理(Promise・async/await)

  • 同期処理と非同期処理の違い、イベントループの仕組みを理解する
  • コールバック関数の問題点(コールバック地獄)を知る
  • Promiseの状態遷移と .then().catch() によるチェーンを書けるようになる
  • async/await で非同期処理を読みやすく書く方法を学ぶ
  • Promise.all() による並列実行と fetch API の実践的な使い方を身につける

JavaScriptはシングルスレッドで動くため、時間のかかる処理(ファイル読み込み・ネットワーク通信・タイマーなど)を同期的に待ってしまうとブラウザ全体が固まってしまう。そこでJavaScriptは 非同期処理 という仕組みを持っている。

同期処理(Synchronous):
[処理A] → [処理B(遅い)] → [処理C]
ここで待つ ↑
非同期処理(Asynchronous):
[処理A] → [処理Bを開始して返る] → [処理C] → ... → [処理Bの結果が来たら処理D]
↑ 待たない

JavaScriptの非同期の仕組みはイベントループによって実現される。

┌─────────────────────────────────────────┐
│ コールスタック(実行中のコード) │
│ ┌──────────────────────────────────┐ │
│ │ 現在実行中の関数 │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
↑ 空になったら取り出す
┌─────────────────────────────────────────┐
│ タスクキュー(実行待ちのコールバック) │
│ [タイマーのコールバック] │
│ [XHRのコールバック] │
│ [クリックイベントのハンドラ] ... │
└─────────────────────────────────────────┘
↑ 非同期APIが完了したらここに積まれる
┌─────────────────────────────────────────┐
│ Web API / Node.js API │
│ setTimeout / fetch / fs.readFile など │
└─────────────────────────────────────────┘

Promise のコールバックは先に動く

Section titled “Promise のコールバックは先に動く”

setTimeout だけでなく、Promise の .then()await の続きもイベントループの対象になる。ただし Promise のコールバックは、通常のタイマーより先に処理される。

同期処理
Promise のコールバック
setTimeout などのタイマー
console.log("1");
setTimeout(() => console.log("3"), 0);
Promise.resolve().then(() => console.log("2"));
console.log("1.5");
// 出力: 1, 1.5, 2, 3

この「Promise 側の先に実行される待ち行列」を microtask queue と呼ぶ。最初は名前を厳密に覚えるより、「Promise の続きは setTimeout(..., 0) より先に動く」と理解しておけば十分である。


非同期処理の最も古い書き方。「処理が終わったら呼ぶ関数」を渡す。

setTimeout(() => {
console.log("1秒後に実行");
}, 1000);
console.log("これが先に実行される");
// 出力順:
// "これが先に実行される"
// (1秒後)"1秒後に実行"

コールバックの問題点:ネストが深くなる(コールバック地獄)。

// コールバック地獄の例
fetchUser(userId, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
// どんどんネストが深くなる...
processComments(comments, (result) => {
console.log(result);
});
});
});
});

コールバック地獄を解決するために導入された仕組み。非同期処理の「将来の結果」を表すオブジェクト。

Promise の状態
┌─────────────────────────────────────────┐
│ pending(保留中) │
│ ↓ 成功 ↓ 失敗 │
│ fulfilled(成功) rejected(失敗) │
│ .then()で受け取る .catch()で受け取る │
└─────────────────────────────────────────┘
// Promiseを作る
const promise = new Promise((resolve, reject) => {
// 非同期処理
setTimeout(() => {
const success = true;
if (success) {
resolve("成功!"); // 成功時
} else {
reject(new Error("失敗!")); // 失敗時
}
}, 1000);
});
// Promiseを使う
promise
.then(result => {
console.log(result); // "成功!"
return "次の処理へ"; // .then()は新しいPromiseを返すのでチェーンできる
})
.then(msg => {
console.log(msg); // "次の処理へ"
})
.catch(error => {
console.error(error.message); // エラー処理
})
.finally(() => {
console.log("成功でも失敗でも最後に実行");
});

.then() は新しいPromiseを返すため、コールバック地獄を平坦に書き直せる。

// コールバック地獄をPromiseで書き直す
fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => processComments(comments))
.then(result => console.log(result))
.catch(error => console.error(error));
// 全て成功したら続行(並列実行)
Promise.all([
fetch("/api/users"),
fetch("/api/products"),
fetch("/api/orders"),
])
.then(([users, products, orders]) => {
// 全てのリクエストが完了してから実行
})
.catch(error => {
// どれか1つでも失敗したら catch される
});
// 最も早く完了したものを使う
Promise.race([slowFetch(), fastFetch()])
.then(result => console.log("最初に終わったもの:", result));
// 全部の結果を受け取る(失敗しても止まらない)
Promise.allSettled([
Promise.resolve("成功"),
Promise.reject("失敗"),
Promise.resolve("成功2"),
])
.then(results => {
results.forEach(r => {
if (r.status === "fulfilled") console.log("成功:", r.value);
else console.log("失敗:", r.reason);
});
});

Promiseをさらに読みやすく書くための構文。同期処理のように見えるコードで非同期処理を書ける。

// async 関数は必ず Promise を返す
async function fetchUserData(userId) {
// await は Promise の解決を待つ
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user; // Promise.resolve(user) と同じ
}
// 使う側
fetchUserData(1)
.then(user => console.log(user))
.catch(error => console.error(error));
// または async 関数の中で await
async function main() {
const user = await fetchUserData(1);
console.log(user);
}

エラーハンドリング(try/catch)

Section titled “エラーハンドリング(try/catch)”

async/await では try/catch でエラーを捕捉できる。

async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("データ取得失敗:", error.message);
throw error; // 呼び出し元にエラーを伝播させる
}
}
// 順次実行(遅い):userを取得してからpostsを取得
async function sequential() {
const user = await fetchUser(1); // 1秒かかる
const posts = await fetchPosts(1); // 1秒かかる
return { user, posts }; // 合計2秒
}
// 並列実行(速い):同時に取得
async function parallel() {
const [user, posts] = await Promise.all([
fetchUser(1), // 同時に開始
fetchPosts(1), // 同時に開始
]);
return { user, posts }; // 最大1秒(同時に走るため)
}
async function bad() {
const result = fetch("/api/data"); // await を忘れた!
console.log(result); // Promise オブジェクトが出力される(データではない)
}
async function good() {
const response = await fetch("/api/data");
const result = await response.json();
console.log(result); // 実際のデータが出力される
}

実践例:fetch APIでデータを取得する

Section titled “実践例:fetch APIでデータを取得する”

第10章では実際のAPIサーバーと連携するが、ここでは公開APIを使って非同期処理の流れを体験してみよう。

// Node.js 18以降は fetch が使えるが、ブラウザの開発者ツールで試すのが簡単
async function getUser(id) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) throw new Error(`${response.status}`);
return await response.json();
} catch (error) {
console.error("取得失敗:", error.message);
return null;
}
}
async function getUsersInParallel(ids) {
const users = await Promise.all(ids.map(id => getUser(id)));
return users.filter(user => user !== null); // 失敗したものを除外
}
// 実行
(async () => {
const user = await getUser(1);
console.log(user.name); // "Leanne Graham"
const users = await getUsersInParallel([1, 2, 3]);
console.log(users.map(u => u.name));
})();

(async () => { ... })() は即時実行の非同期関数(IIFE)。await はトップレベルの async 関数の中でしか使えないため、このパターンが使われる(Node.js 14以降ではESモジュールでトップレベルの await が使えるようになった)。


項目ポイント
コールバック古い書き方。ネストが深くなる(コールバック地獄)
Promise非同期処理の「将来の結果」。.then().catch() でチェーン
async/awaitPromiseをわかりやすく書く構文。try/catch でエラー処理
並列実行Promise.all() で複数の非同期処理を同時に走らせる
注意点await を忘れるとPromiseオブジェクトになってしまう