JavaScript Promise入門
非同期処理を理解する
JavaScriptのPromiseの仕組みを基礎から解説。コールバック地獄の解決、Promiseチェーン、Promise.all、async/awaitまでステップごとに学べます。
こんな人向けの記事です
- JavaScriptの非同期処理を基礎から理解したい
- Promiseの仕組みを正確に知りたい
- async/awaitを使いこなしたい
Step 1非同期処理とは(同期 vs 非同期)
JavaScriptはシングルスレッドの言語です。つまり、一度に1つの処理しか実行できません。しかし、API通信やファイル読み込みなど時間のかかる処理を待っている間、画面が固まってしまっては困ります。そこで「非同期処理」が必要になります。
同期処理:上から順番に実行し、1つの処理が終わるまで次に進まない。
非同期処理:時間のかかる処理を「後で結果を受け取る」形にして、その間に別の処理を進める。
// 同期処理:上から順番に実行される
console.log("1. 開始");
console.log("2. 処理中...");
console.log("3. 完了");
// 出力順: 1 → 2 → 3
// 非同期処理:setTimeoutは後回しにされる
console.log("1. 開始");
setTimeout(() => {
console.log("2. 非同期処理完了");
}, 1000);
console.log("3. 次の処理");
// 出力順: 1 → 3 → 2(2は1秒後に出力される)
setTimeout はブラウザに「1秒後にこの関数を実行して」と依頼するだけで、JavaScript自体は次の行に進みます。これが非同期処理の基本的な動きです。
| 比較項目 | 同期処理 | 非同期処理 |
|---|---|---|
| 実行順序 | 上から順番に1つずつ | 待たずに次の処理へ進む |
| ブロッキング | 前の処理が終わるまで止まる | 止まらない |
| 代表例 | 変数代入、計算 | API通信、タイマー、ファイル読み込み |
| 結果の取得 | 戻り値で即座に取得 | コールバック・Promise等で後から取得 |
Step 2コールバック地獄の問題
Promiseが登場する前、非同期処理の結果を受け取るにはコールバック関数を使うのが一般的でした。しかし、非同期処理が連鎖するとコードが右に深くネストされていく「コールバック地獄(Callback Hell)」が発生します。
// ユーザー情報を取得 → 注文を取得 → 商品を取得...
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
getProduct(detail.productId, function(product) {
console.log(product.name);
// さらにネストが続く...
}, function(err) {
console.error("商品取得エラー:", err);
});
}, function(err) {
console.error("注文詳細取得エラー:", err);
});
}, function(err) {
console.error("注文一覧取得エラー:", err);
});
}, function(err) {
console.error("ユーザー取得エラー:", err);
});
- 可読性が低い:ネストが深くなりコードが右に伸びていく
- エラーハンドリングが煩雑:各コールバックごとにエラー処理が必要
- 制御フローが複雑:処理の順番が追いにくい
- 保守性が低い:変更・修正が困難
この問題を根本的に解決するために導入されたのが Promise です。ES2015(ES6)で正式に仕様に追加され、非同期処理をフラットに書けるようになりました。
Step 3Promiseの基本(new Promise, then, catch, finally)
Promiseは非同期処理の「最終的な結果」を表すオブジェクトです。Promiseは以下の3つの状態のいずれかを持ちます。
| 状態 | 意味 | 遷移条件 |
|---|---|---|
pending | 待機中(初期状態) | 作成直後 |
fulfilled | 成功(解決済み) | resolve() が呼ばれた |
rejected | 失敗(拒否済み) | reject() が呼ばれた |
Promiseの状態は pending から fulfilled または rejected に一度だけ変わります。一度確定した状態は二度と変わりません。これを「settled(確定)」と呼びます。
// Promiseの作成
const myPromise = new Promise((resolve, reject) => {
// 非同期処理をここに書く
const success = true;
if (success) {
resolve("処理が成功しました"); // fulfilled状態にする
} else {
reject(new Error("処理が失敗しました")); // rejected状態にする
}
});
// Promiseの使用
myPromise
.then((result) => {
// fulfilledの時に実行される
console.log(result); // "処理が成功しました"
})
.catch((error) => {
// rejectedの時に実行される
console.error(error.message);
})
.finally(() => {
// 成功・失敗に関わらず必ず実行される
console.log("処理が完了しました");
});
実用的な例として、APIからデータを取得する関数をPromiseで作ってみましょう。
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
// XMLHttpRequestによる通信(レガシーな方法)
const xhr = new XMLHttpRequest();
xhr.open("GET", `/api/users/${userId}`);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP Error: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("ネットワークエラー"));
xhr.send();
});
}
// 使用例
fetchUserData(1)
.then(user => console.log(user.name))
.catch(err => console.error("取得失敗:", err.message));
new Promise のコールバック内で return を使っても意味がありません。必ず resolve() または reject() を呼んで状態を変更してください。また、resolve/reject を呼んだ後も処理は続行されるため、必要なら return で以降の処理を止めましょう。
Step 4Promiseチェーン
then() は新しいPromiseを返すため、.then().then().then() のようにチェーンできます。これにより、Step 2で見たコールバック地獄をフラットに書き直せます。
// コールバック地獄だったコードをPromiseチェーンで書き直す
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => getProduct(detail.productId))
.then(product => {
console.log(product.name);
})
.catch(err => {
// どの段階のエラーもここで一括キャッチ
console.error("エラー:", err.message);
});
ネストが消え、処理の流れが上から下に一直線になりました。エラーハンドリングも catch 1箇所で済みます。
then のコールバックが返す値によって、次の then に渡される値が変わります。
- 普通の値を返す:その値がそのまま次の
thenに渡される - Promiseを返す:そのPromiseが解決された値が次の
thenに渡される - 何も返さない:
undefinedが次のthenに渡される
// 値を変換しながらチェーンする
fetch("/api/users/1")
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // Promiseを返す
})
.then(user => {
console.log(user.name);
return user.name.toUpperCase(); // 普通の値を返す
})
.then(upperName => {
console.log(upperName); // 大文字に変換された名前
})
.catch(err => {
console.error("エラー:", err.message);
});
then の中で return を忘れると、チェーンが「切れて」次の then に undefined が渡されます。非同期処理を含む場合は特に return を忘れないようにしましょう。
Step 5Promise.all / Promise.race / Promise.allSettled
複数の非同期処理を並行して実行したい場合に便利な静的メソッドがあります。
Promise.all:すべて成功したら結果をまとめる
// 3つのAPIを同時に呼ぶ
const userPromise = fetch("/api/users/1").then(r => r.json());
const postsPromise = fetch("/api/posts?userId=1").then(r => r.json());
const settingsPromise = fetch("/api/settings").then(r => r.json());
Promise.all([userPromise, postsPromise, settingsPromise])
.then(([user, posts, settings]) => {
// 3つすべてが成功した時だけ実行される
console.log("ユーザー:", user.name);
console.log("投稿数:", posts.length);
console.log("テーマ:", settings.theme);
})
.catch(err => {
// 1つでも失敗したらここに来る
console.error("いずれかのAPIが失敗:", err.message);
});
Promise.race:最初に確定したものを採用
// タイムアウト付きfetch
function fetchWithTimeout(url, timeoutMs) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("タイムアウト")), timeoutMs);
});
// 先に確定した方が結果になる
return Promise.race([fetchPromise, timeoutPromise]);
}
fetchWithTimeout("/api/slow-endpoint", 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error(err.message)); // "タイムアウト"
Promise.allSettled:全部の結果を取得(成功・失敗問わず)
const promises = [
fetch("/api/users/1").then(r => r.json()),
fetch("/api/users/999").then(r => {
if (!r.ok) throw new Error("Not Found");
return r.json();
}),
fetch("/api/users/2").then(r => r.json()),
];
Promise.allSettled(promises).then(results => {
results.forEach((result, i) => {
if (result.status === "fulfilled") {
console.log(`#${i} 成功:`, result.value.name);
} else {
console.log(`#${i} 失敗:`, result.reason.message);
}
});
});
// #0 成功: Alice
// #1 失敗: Not Found
// #2 成功: Bob
| メソッド | 挙動 | 使いどころ |
|---|---|---|
Promise.all | 全て成功で解決、1つでも失敗で拒否 | 全てのデータが揃わないと表示できない場面 |
Promise.race | 最初に確定したものを採用 | タイムアウト実装、最速レスポンスの採用 |
Promise.allSettled | 全て確定するまで待ち、成功・失敗の結果を配列で返す | 一部失敗しても残りの結果が欲しい場面 |
Promise.any | 最初に成功したものを採用(全失敗で拒否) | 複数のミラーサーバーから最速で取得 |
Step 6async/awaitでPromiseをシンプルに書く
ES2017で導入された async/await は、Promiseを同期処理のように書ける構文です。Promiseチェーンよりさらに読みやすいコードになります。
// Promiseチェーン版
function getUserData(userId) {
return getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => getProduct(detail.productId));
}
// async/await版(同じ処理)
async function getUserData(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
const product = await getProduct(detail.productId);
return product;
}
async 関数は常にPromiseを返します。await はPromiseの解決を待ってから値を取り出します。内部的にはPromiseチェーンと同じ動きですが、構文が同期的になるため可読性が格段に上がります。
エラーハンドリング:try/catch
async function fetchUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const user = await response.json();
console.log("ユーザー名:", user.name);
return user;
} catch (error) {
console.error("取得失敗:", error.message);
return null; // デフォルト値を返す
} finally {
console.log("API呼び出し完了");
}
}
並行実行:await + Promise.all
async function loadDashboard(userId) {
// 悪い例:順番に実行される(遅い)
// const user = await fetchUser(userId);
// const posts = await fetchPosts(userId);
// const notifications = await fetchNotifications(userId);
// 良い例:並行に実行される(速い)
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId),
]);
return { user, posts, notifications };
}
await を連続で書くと、前の処理が終わるまで次が始まりません。互いに依存しない処理は Promise.all で並行実行しましょう。3つのAPI呼び出しが各1秒かかる場合、逐次なら3秒、並行なら約1秒で済みます。
ループ内でのawait
const userIds = [1, 2, 3, 4, 5];
// 逐次実行(順番に処理したい場合)
async function fetchUsersSequential(ids) {
const users = [];
for (const id of ids) {
const user = await fetchUser(id);
users.push(user);
}
return users;
}
// 並行実行(順番を気にしない場合)
async function fetchUsersParallel(ids) {
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
return users;
}
この記事のまとめ
- JavaScriptはシングルスレッドのため、非同期処理で待ち時間を有効活用する
- コールバックの連鎖は「コールバック地獄」を引き起こす
- Promiseは非同期処理の結果を表すオブジェクトで、
then/catch/finallyで扱う - Promiseチェーンによりネストをフラットに書ける
Promise.allで並行実行、Promise.raceでタイムアウト、Promise.allSettledで全結果取得async/awaitはPromiseを同期風に書ける構文で、try/catchでエラーを処理する- 独立した非同期処理は
await Promise.all()で並行実行して高速化する