8-4. 非同期処理(Promise・async/await)
このセクションで学ぶこと
Section titled “このセクションで学ぶこと”- 同期処理と非同期処理の違い、イベントループの仕組みを理解する
- コールバック関数の問題点(コールバック地獄)を知る
- Promiseの状態遷移と
.then()・.catch()によるチェーンを書けるようになる async/awaitで非同期処理を読みやすく書く方法を学ぶPromise.all()による並列実行と fetch API の実践的な使い方を身につける
非同期処理とは
Section titled “非同期処理とは”JavaScriptはシングルスレッドで動くため、時間のかかる処理(ファイル読み込み・ネットワーク通信・タイマーなど)を同期的に待ってしまうとブラウザ全体が固まってしまう。そこでJavaScriptは 非同期処理 という仕組みを持っている。
同期処理(Synchronous):[処理A] → [処理B(遅い)] → [処理C] ここで待つ ↑
非同期処理(Asynchronous):[処理A] → [処理Bを開始して返る] → [処理C] → ... → [処理Bの結果が来たら処理D] ↑ 待たないイベントループ
Section titled “イベントループ”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) より先に動く」と理解しておけば十分である。
コールバック関数
Section titled “コールバック関数”非同期処理の最も古い書き方。「処理が終わったら呼ぶ関数」を渡す。
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
Section titled “Promise”コールバック地獄を解決するために導入された仕組み。非同期処理の「将来の結果」を表すオブジェクト。
Promiseの状態
Section titled “Promiseの状態”Promise の状態┌─────────────────────────────────────────┐│ pending(保留中) ││ ↓ 成功 ↓ 失敗 ││ fulfilled(成功) rejected(失敗) ││ .then()で受け取る .catch()で受け取る │└─────────────────────────────────────────┘Promiseの基本的な使い方
Section titled “Promiseの基本的な使い方”// 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("成功でも失敗でも最後に実行"); });Promiseチェーン
Section titled “Promiseチェーン”.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を同時に扱う
Section titled “複数のPromiseを同時に扱う”// 全て成功したら続行(並列実行)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); }); });async / await
Section titled “async / await”Promiseをさらに読みやすく書くための構文。同期処理のように見えるコードで非同期処理を書ける。
基本的な使い方
Section titled “基本的な使い方”// 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 関数の中で awaitasync 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; // 呼び出し元にエラーを伝播させる }}並列実行と順次実行
Section titled “並列実行と順次実行”// 順次実行(遅い):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秒(同時に走るため)}よくある間違い:await の忘れ
Section titled “よくある間違い:await の忘れ”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/await | Promiseをわかりやすく書く構文。try/catch でエラー処理 |
| 並列実行 | Promise.all() で複数の非同期処理を同時に走らせる |
| 注意点 | await を忘れるとPromiseオブジェクトになってしまう |