
JavaScriptのPromise
2025-07-28はじめに
前章までで、非同期処理の基本とコールバック関数について学びました。コールバック関数には「コールバック地獄」やエラー処理の難しさなどの問題があることを理解したと思います。この章では、これらの問題を解決するためにES6(ES2015)で導入された「Promise」について詳しく解説します。Promiseは現代JavaScriptの非同期処理の基盤となる重要な概念で、async/awaitの基礎にもなっています。
Promiseとは?
基本的な定義
Promise(プロミス)は、非同期処理の最終的な完了(または失敗)とその結果の値を表現するオブジェクトです。日本語で「約束」という意味通り、「将来のいつか値が取得できることを約束する」という概念です。
3つの状態
Promiseオブジェクトは以下の3つの状態のいずれかを取ります:
- pending(待機): 初期状態。履行も拒否もされていない
- fulfilled(履行): 処理が成功して完了した状態
- rejected(拒否): 処理が失敗した状態
一度fulfilledまたはrejectedになると、Promiseの状態はそれ以降変化しません(不変性)。
シンプルな例
const myPromise = new Promise((resolve, reject) => {
// 非同期処理を実行
setTimeout(() => {
const success = true; // ここをfalseにするとrejectが呼ばれる
if (success) {
resolve("処理が成功しました!");
} else {
reject("エラーが発生しました");
}
}, 1000);
});
myPromise
.then((result) => {
console.log(result); // "処理が成功しました!"
})
.catch((error) => {
console.error(error); // "エラーが発生しました"
});
なぜPromiseが必要なのか?
コールバック関数の問題点
- コールバック地獄: ネストが深くなり、コードが読みにくい
- エラー処理の分散: try-catchが使えず、エラー処理がバラバラになる
- フロー制御の難しさ: 並列処理や直列処理の管理が複雑
- 信頼性の問題: コールバックが複数回呼ばれたり、全く呼ばれない可能性
Promiseのメリット
- チェーン可能:
.then()
でつなげて読みやすいコードになる - 統一的エラー処理: チェーンの最後に
.catch()
で一括処理可能 - フロー制御の容易さ:
Promise.all()
など便利なメソッドが用意されている - 信頼性: 状態が不変で、一度解決すると変化しない
Promiseの基本的な使い方
Promiseの作成
const promise = new Promise((resolve, reject) => {
// 非同期処理を実行
// 成功時: resolve(値)を呼び出す
// 失敗時: reject(エラー)を呼び出す
});
Promiseの消費
promise
.then((result) => {
// 成功時の処理
})
.catch((error) => {
// 失敗時の処理
})
.finally(() => {
// 成功・失敗に関わらず実行される処理
});
Promiseチェーン
Promiseの真価は、複数の非同期処理を連結できる点にあります。
function asyncTask1() {
return new Promise((resolve) => {
setTimeout(() => resolve("結果1"), 1000);
});
}
function asyncTask2(data) {
return new Promise((resolve) => {
setTimeout(() => resolve(data + " → 結果2"), 1000);
});
}
function asyncTask3(data) {
return new Promise((resolve) => {
setTimeout(() => resolve(data + " → 結果3"), 1000);
});
}
asyncTask1()
.then(asyncTask2)
.then(asyncTask3)
.then((finalResult) => {
console.log(finalResult); // "結果1 → 結果2 → 結果3"
})
.catch((error) => {
console.error("エラー発生:", error);
});
このように、.then()
でつなげていくことで、非同期処理を同期処理のように直列的に記述できます。
エラー処理
Promiseでは.catch()
を使ってエラーを捕捉します。チェーンのどこでエラーが発生しても、最後の.catch()
で処理できます。
function mightFail() {
return new Promise((resolve, reject) => {
const random = Math.random();
if (random > 0.5) {
resolve("成功!");
} else {
reject("失敗...");
}
});
}
mightFail()
.then((result) => {
console.log(result);
return mightFail(); // もう一度実行
})
.then((result) => {
console.log("2回目も成功:", result);
})
.catch((error) => {
console.error("エラー:", error); // どちらかのmightFailが失敗するとここで捕捉
});
Promiseの静的メソッド
Promiseには便利な静的メソッドが用意されています。
Promise.all()
複数のPromiseを並列実行し、すべてが成功したら処理を続行します。
const promise1 = fetch("/api/data1");
const promise2 = fetch("/api/data2");
const promise3 = fetch("/api/data3");
Promise.all([promise1, promise2, promise3])
.then((results) => {
// resultsは[data1, data2, data3]の配列
console.log("すべてのデータ取得完了:", results);
})
.catch((error) => {
// 1つでも失敗するとここに入る
console.error("いずれかの取得に失敗:", error);
});
Promise.race()
複数のPromiseのうち、最初に完了したもの(成功・失敗問わず)の結果を使います。
const timeout = new Promise((_, reject) => {
setTimeout(() => reject("タイムアウト"), 5000);
});
const fetchPromise = fetch("/api/data");
Promise.race([fetchPromise, timeout])
.then((data) => {
console.log("データ取得成功:", data);
})
.catch((error) => {
console.error("エラー:", error); // 取得が5秒以上かかるとタイムアウト
});
Promise.allSettled()
すべてのPromiseが完了(成功または失敗)するのを待ちます。
const promises = [
fetch("/api/data1"),
fetch("/api/data2").catch(() => "デフォルト値"),
Promise.reject("明示的なエラー")
];
Promise.allSettled(promises)
.then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("成功:", result.value);
} else {
console.log("失敗:", result.reason);
}
});
});
Promiseの実践的使用例
例1: fetch APIを使ったHTTPリクエスト
function fetchUserData(userId) {
return fetch(`https://api.example.com/users/${userId}`)
.then((response) => {
if (!response.ok) {
throw new Error("ネットワークレスポンスが正常ではありません");
}
return response.json();
})
.then((user) => {
console.log("ユーザーデータ:", user);
return user;
});
}
fetchUserData(123)
.catch((error) => {
console.error("データ取得に失敗:", error);
});
例2: タイムアウト処理
function withTimeout(promise, timeoutMs) {
const timeout = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`操作がタイムアウトしました(${timeoutMs}ms)`));
}, timeoutMs);
});
return Promise.race([promise, timeout]);
}
// 使用例
const slowFetch = fetch("/api/data").then((r) => r.json());
withTimeout(slowFetch, 3000)
.then((data) => console.log("データ:", data))
.catch((error) => console.error("エラー:", error));
Promiseのよくある間違い
間違い1: Promiseコンストラクタの誤用
// 悪い例
function getData() {
return new Promise((resolve) => {
fetch("/api/data").then(resolve);
});
}
// 良い例
function getData() {
return fetch("/api/data");
}
間違い2: エラー処理の忘れ
// 悪い例
fetch("/api/data")
.then((response) => response.json());
// 良い例
fetch("/api/data")
.then((response) => response.json())
.catch((error) => console.error("エラー:", error));
間違い3: Promiseチェーンの分断
// 悪い例
const promise = fetch("/api/data")
.then((response) => response.json());
promise.then((data) => console.log(data));
promise.then((data) => processData(data)); // 別のチェーンになる
// 良い例
fetch("/api/data")
.then((response) => response.json())
.then((data) => {
console.log(data);
return processData(data);
});
Promiseのベストプラクティス
- 常にエラー処理を行う:
.catch()
かtry/catch
(async/await時)を必ず使う - Promiseを適切に返す: チェーンを分断しない
- 不必要なネストを避ける:
.then()
はフラットに保つ - 名前付き関数を使う: 複雑な処理は関数として定義
- Promise化を活用: コールバックスタイルのAPIをPromiseでラップ
Promiseとasync/awaitの関係
次章で詳しく学ぶasync/awaitはPromiseのシンタックスシュガー(構文糖)です。async関数は常にPromiseを返し、awaitはPromiseの解決を待ちます。
async function fetchData() {
try {
const response = await fetch("/api/data");
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error("エラー:", error);
throw error;
}
}
// async関数はPromiseを返すので、then/catchも使える
fetchData().then((data) => processData(data));
まとめ
Promiseについての重要なポイント:
- Promiseは非同期処理の最終的な完了(成功/失敗)とその結果を表現するオブジェクト
- pending(待機)、fulfilled(履行)、rejected(拒否)の3つの状態がある
.then()
で成功時の処理、.catch()
でエラー処理を記述- チェーンすることで複数の非同期処理を順番に実行できる
Promise.all()
、Promise.race()
などの便利な静的メソッドがある- コールバック地獄の問題を解決できる
- async/awaitの基礎となる概念
- 現代JavaScriptの非同期処理の基盤
Promiseをマスターすることで、非同期コードをより宣言的で管理しやすい形で記述できるようになります。
練習問題
問題1
以下のコードの実行順序と出力結果を予想してください。
console.log("スクリプト開始");
const promise = new Promise((resolve) => {
console.log("Promise実行");
setTimeout(() => {
resolve("Promise解決");
console.log("タイマーコールバック");
}, 0);
});
promise.then((msg) => {
console.log(msg);
});
console.log("スクリプト終了");
問題2
次のPromiseの特徴について、正しいものには○、間違っているものには×をつけてください。
- Promiseの状態は一度解決すると変化しない ( )
.then()
メソッドは新しいPromiseを返す ( )Promise.all()
は渡されたPromiseのうち1つでも失敗すると即時 reject する ( ).finally()
コールバックは解決値や拒否理由を受け取ることができる ( )
問題3
以下のコールバックスタイルの関数をPromiseでラップするコードを書いてください。
function readFile(path, encoding, callback) {
// ファイルを読み込む非同期処理
// 成功時: callback(null, data)
// 失敗時: callback(error)
}
解答例
問題1の解答
スクリプト開始
Promise実行
スクリプト終了
タイマーコールバック
Promise解決
説明:
- 同期的なコードが最初に実行される
- Promiseコンストラクタ内の同期的なコード(console.log)が実行
- setTimeoutは非同期なので後回し
- スクリプトの同期的な実行が終了
- マイクロタスク(Promise)よりも先にマクロタスク(setTimeout)が実行
- Promiseが解決され、thenのコールバックが実行
問題2の解答
- ○
- ○
- ○
- × (finallyコールバックは引数を受け取らない)
問題3の解答
function readFilePromise(path, encoding) {
return new Promise((resolve, reject) => {
readFile(path, encoding, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// 使用例
readFilePromise("example.txt", "utf8")
.then((data) => console.log(data))
.catch((error) => console.error("読み込みエラー:", error));