JavaScriptの基礎 - Promise
概要
Promiseは、非同期処理の最終的な完了または失敗を表すオブジェクトである。
ES2015 (ES6) で導入され、コールバック関数によって引き起こされるコールバック地獄を解決するための仕組みとして設計された。
Promiseを使用すると、非同期処理の成功・失敗を統一されたインターフェースで扱うことができる。
処理の結果を .then チェーンで連結することにより、非同期処理を同期処理と同様の可読性で記述できる。
Promiseの主な特徴は以下の通りである。
- 状態管理
- pending (待機)、fulfilled (履行)、rejected (拒否) の3状態を持つ
- 一度settled (確定) した状態は変更されない
- チェーン接続
.then、.catch、.finallyメソッドで処理を連結できる- 各メソッドは新しいPromiseを返すため、チェーンが構築できる
- 静的メソッド
Promise.all、Promise.race等、複数のPromiseを組み合わせるメソッドを持つ
なお、Promiseをより簡潔に記述する async / await 構文については、JavaScriptの基礎 - async awaitのページを参照すること。
Promiseとは
Promiseの状態
Promiseは作成された時点から、以下の3つのいずれかの状態を持つ。
| 状態 | 意味 | 値の有無 |
|---|---|---|
| pending (待機) | 初期状態 処理が完了していない状態 |
値なし |
| fulfilled (履行) | 処理が成功して完了した状態 | value (結果値) を持つ |
| rejected (拒否) | 処理が失敗した状態 | reason (拒否理由) を持つ |
pendingからfulfilled または rejected へ遷移すると、その後状態は変化しない。
fulfilled および rejected の状態をまとめて settled (確定) と呼ぶ。
状態遷移は一方向であり、1度settledになったPromiseの状態を後から変更することはできない。
new Promise
new Promise(executor) でPromiseオブジェクトを作成する。
executorは、Promise生成時に同期的に呼び出される関数であり、resolve と reject の2つの引数を受け取る。
基本構文を以下に示す。
const promise = new Promise((resolve, reject) => {
// 非同期処理または同期処理を記述する
if (/* 処理が成功 */ true) {
resolve(value); // fulfilled状態に変更する
}
else {
reject(reason); // rejected状態に変更する
}
});
executor内での各関数の動作を以下に示す。
| 引数 | 説明 |
|---|---|
resolve(value)
|
|
reject(reason)
|
|
executor内で例外が発生した場合、resolve / reject がまだ呼ばれていなければ、Promiseは自動的にrejected状態となる。
実用的な使用例を以下に示す。
// n秒後に値を返すPromiseを作成する
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve(ms), ms);
});
}
// 成功と失敗の両方を持つPromiseの例
function fetchData(shouldFail) {
return new Promise((resolve, reject) => {
if (shouldFail) {
reject(new Error("データの取得に失敗した"));
}
else {
resolve({ id: 1, name: "Alice" });
}
});
}
// 使用例
delay(1000).then(ms => console.log(ms + "ミリ秒が経過した"));
// 1秒後: "1000ミリ秒が経過した"
thenチェーン
.then
.then(onFulfilled, onRejected) は、Promiseがsettledになった後に実行するコールバックを登録するメソッドである。
常に新しいPromiseを返すため、複数の .then を連結してチェーンを構築できる。
| 引数 | 説明 |
|---|---|
onFulfilled
|
|
onRejected
|
|
.then の戻り値のルールを以下に示す。
| 条件 | 挙動 |
|---|---|
| コールバックが値を返した場合 | その値でfulfilledになる新しいPromiseを返す。 |
| コールバックがPromiseを返した場合 | その返されたPromiseが解決されるまで待機する。 |
| コールバックで例外が発生した場合 | その例外でrejectedになる新しいPromiseを返す。 |
使用例を以下に示す。
// .thenチェーンで値を変換しながら伝播する
Promise.resolve(1)
.then(value => {
console.log(value); // 1
return value + 1; // 次のthenにvalue=2が渡される
})
.then(value => {
console.log(value); // 2
return value * 10; // 次のthenにvalue=20が渡される
})
.then(value => {
console.log(value); // 20
});
// Promiseを返すことで非同期処理を直列につなぐ
function fetchUser(id) {
return Promise.resolve({ id, name: "Alice" });
}
function fetchPosts(userId) {
return Promise.resolve([{ id: 1, title: "最初の投稿", userId }]);
}
fetchUser(1)
.then(user => {
console.log("ユーザ:", user.name);
return fetchPosts(user.id); // Promiseを返す
})
.then(posts => {
console.log("投稿数:", posts.length);
});
.catch
.catch(onRejected) は、Promiseがrejected状態になった時のコールバックを登録するメソッドである。
.then(null, onRejected) と同等であり、チェーン内の任意の位置で発生したエラーをキャッチできる。
onRejected 内で値を返した場合、その値でfulfilledになる新しいPromiseを返す。
これにより、エラーから回復してチェーンを継続することができる。
// エラー回復の例
Promise.reject(new Error("最初のエラー"))
.catch(error => {
console.log("エラーをキャッチ:", error.message);
return "回復した値"; // エラーから回復してチェーンを継続する
})
.then(value => {
console.log("回復後の値:", value); // "回復した値"
});
// チェーン内のエラーをまとめてキャッチする
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.catch(error => {
// どのステップで発生したエラーもここでキャッチできる
console.error("処理に失敗した:", error.message);
});
.finally
.finally(onFinally) は、Promiseがfulfilled または rejected のいずれの状態になった場合も実行されるコールバックを登録するメソッドである。
.finally の特徴を以下に示す。
onFinallyは引数を受け取らない。(成功・失敗の結果は渡されない)- 元のPromiseの状態と結果値がそのまま保持される。(通過する)
- リソースの解放やローディング状態の解除等、クリーンアップ処理に使用する。
// ローディング表示のクリーンアップ例
function fetchWithLoading(url) {
showLoadingSpinner(); // ローディング表示開始
return fetch(url)
.then(response => response.json())
.catch(error => {
console.error("取得に失敗した:", error.message);
throw error; // エラーを再スローして呼び出し元に伝える
})
.finally(() => {
hideLoadingSpinner(); // 成功・失敗に関わらずローディングを非表示にする
});
}
// .finallyの戻り値は元のPromiseの結果を引き継ぐ
Promise.resolve("成功値")
.finally(() => {
console.log("finallyが実行された");
// return "別の値"; // この戻り値は無視される
})
.then(value => {
console.log(value); // "成功値" (元の値が保持される)
});
チェーンの動作原理
.then、.catch、.finally は常に新しいPromiseを返す。
この性質により、各メソッドをチェーン状に連結することができる。
コールバックの戻り値が次のPromiseの状態を決定する仕組みを以下に示す。
// チェーンの動作原理を示す例
const p1 = Promise.resolve("初期値");
const p2 = p1.then(value => {
// 通常の値を返す → p2はその値でfulfilledになる
return value + " -> 変換後";
});
const p3 = p2.then(value => {
// Promiseを返す → p3はそのPromiseの解決を待つ
return new Promise(resolve => {
setTimeout(() => resolve(value + " -> 非同期処理後"), 100);
});
});
const p4 = p3.then(value => {
// 例外を投げる → p4はrejectedになる
throw new Error("意図的なエラー");
});
const p5 = p4.catch(error => {
// .catchのコールバックが値を返す → p5はfulfilledになる (エラー回復)
return "エラーから回復";
});
p5.then(value => console.log(value)); // "エラーから回復"
エラーはチェーン内を伝播し、最初の .catch に到達するまで各 .then をスキップする。
// エラー伝播の例
Promise.resolve("開始")
.then(value => { throw new Error("ステップ1でエラー"); })
.then(value => {
// このコールバックはスキップされる (エラーが伝播している)
console.log("スキップ:", value);
return value;
})
.catch(error => {
// エラーがここでキャッチされる
console.log("キャッチ:", error.message); // "ステップ1でエラー"
return "回復した";
})
.then(value => {
// .catchの後は通常のチェーンに戻る
console.log("回復後:", value); // "回復した"
});
Promiseの静的メソッド
Promise.resolve / Promise.reject
Promise.resolve(value) は、指定した値でfulfilledになるPromiseを生成するショートカットである。
Promise.reject(reason) は、指定した理由でrejectedになるPromiseを生成するショートカットである。
// Promise.resolve : 値をPromiseに変換する
Promise.resolve(42).then(v => console.log(v)); // 42
Promise.resolve("hello").then(v => console.log(v)); // "hello"
// valueがPromiseの場合はそのまま返す (新しいPromiseでラップしない)
const p = Promise.resolve(42);
console.log(Promise.resolve(p) === p); // true
// Promise.reject : 拒否されたPromiseを生成する
Promise.reject(new Error("失敗")).catch(e => console.log(e.message)); // "失敗"
// 関数をPromiseを返す形式に正規化する際に使用する
function ensurePromise(value) {
return Promise.resolve(value);
}
ensurePromise(42).then(v => console.log(v)); // 42
ensurePromise(Promise.resolve(42)).then(v => console.log(v)); // 42
Promise.all
Promise.all(iterable) は、渡したPromiseの配列が全てfulfilledになるまで待機し、全ての結果を配列で返す。
1つでもrejectedになると、即座にrejectedになる。
// 全てのPromiseが成功する場合
const p1 = Promise.resolve("ユーザ情報");
const p2 = Promise.resolve("注文情報");
const p3 = Promise.resolve("在庫情報");
Promise.all([p1, p2, p3])
.then(([user, order, stock]) => {
// 結果は元の配列の順序を維持する
console.log(user); // "ユーザ情報"
console.log(order); // "注文情報"
console.log(stock); // "在庫情報"
});
// 1つでも失敗すると即座にrejectされる
Promise.all([
Promise.resolve("成功1"),
Promise.reject(new Error("失敗")),
Promise.resolve("成功2"),
])
.catch(error => {
console.log(error.message); // "失敗"
// "成功1"や"成功2"の結果は取得できない
});
Promise.allSettled
Promise.allSettled(iterable) は、渡したPromiseの配列が全てsettledになるまで待機する。
成功・失敗に関わらず全ての結果を収集して、各結果を { status, value } または { status, reason } 形式のオブジェクトの配列で返す。
Promise.allSettled([
Promise.resolve("成功"),
Promise.reject(new Error("失敗")),
Promise.resolve("また成功"),
])
.then(results => {
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("成功:", result.value);
}
else {
console.log("失敗:", result.reason.message);
}
});
});
// 出力:
// "成功: 成功"
// "失敗: 失敗"
// "成功: また成功"
Promise.all と異なり、途中で失敗しても全ての結果を取得できる点がある。
Promise.race
Promise.race(iterable) は、渡したPromiseのうち最初に settled になったものの結果 (fulfilled または rejectedどちらでも) を返す。
// タイムアウト処理の実装例
function fetchWithTimeout(url, timeoutMs) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("タイムアウト")), timeoutMs);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
fetchWithTimeout("https://api.example.com/data", 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.message === "タイムアウト") {
console.log("3秒以内に応答がなかった");
}
else {
console.log("取得に失敗した:", error.message);
}
});
Promise.any
Promise.any(iterable) は、渡したPromiseのうち最初にfulfilledになったものの値を返す。
全てのPromiseがrejectedになった場合のみ、AggregateError でrejectedになる。
// フェイルオーバー (CDN冗長化) の実装例
function fetchFromFastest(urls) {
const requests = urls.map(url => fetch(url));
return Promise.any(requests);
}
fetchFromFastest([
"https://cdn1.example.com/data",
"https://cdn2.example.com/data",
"https://cdn3.example.com/data",
])
.then(response => console.log("最速のCDNから取得成功"))
.catch(error => {
// AggregateErrorには全ての拒否理由が含まれる
console.log("全てのCDNから取得に失敗した");
console.log(error.errors); // 各Promiseの拒否理由の配列
});
Promise.try (ES2025)
Promise.try(fn) は、関数 fn を実行し、その結果を常にPromiseとして返す。
fn が同期的に例外を投げた場合も、rejectedなPromiseとして扱われる点がある。
ブラウザサポートは Chrome 128+、Firefox 127+、Safari 17.6+、Node.js 22.6+ 以降である。
// Promise.tryを使用しない場合: 同期例外がPromiseの拒否として扱われない
// Promise.resolve().then(() => riskySync()) のような回避策が必要だった
// Promise.tryを使用した場合: 同期例外もPromiseの拒否として扱われる
function riskySync() {
throw new Error("同期エラー");
}
Promise.try(() => riskySync())
.catch(error => {
console.log("捕捉:", error.message); // "捕捉: 同期エラー"
});
// 同期・非同期を問わず統一されたエラーハンドリングが実現できる
function processInput(input) {
return Promise.try(() => {
if (typeof input !== "string") {
throw new TypeError("文字列を指定すること"); // 同期例外
}
return fetch("/api/process?q=" + input); // 非同期処理
});
}
processInput(123)
.catch(e => console.log(e.message)); // "文字列を指定すること"
下表に、各静的メソッドの動作の比較を示す。
| メソッド | 完了タイミング | 成功時の戻り値 | 失敗時の動作 | 主な用途 |
|---|---|---|---|---|
| Promise.all | 全て完了まで待機 | 結果の配列 (順序維持) | 最初の拒否で即座にreject | 全て成功が必要な場合 |
| Promise.allSettled | 全て完了まで待機 | status / value / reason オブジェクトの配列 | 全て収集 (rejectしない) | 全ての結果を取得したい場合 |
| Promise.race | 最初に完了したもの | 最初の結果値 | 最初の拒否理由 | タイムアウト処理 |
| Promise.any | 最初に成功したもの | 最初の成功値 | 全て失敗時のみ AggregateError | フェイルオーバー処理 |
| Promise.try | 関数実行後 | 関数の戻り値または結果 | 同期/非同期例外を拒否として扱う | 統一されたエラーハンドリング |
コールバック地獄からPromiseへの移行
コールバックパターンのPromise化
Node.jsスタイルのエラーファーストコールバック (第1引数がエラー、第2引数がデータ) を持つ関数は、Promiseを返す関数に変換 (Promisification) できる。
- コールバック地獄の例
// Before: コールバック地獄 // ネストが深くなりコードの可読性が著しく低下する function getUserData(userId, callback) { fetchUser(userId, function(err, user) { if (err) { callback(err); return; } fetchPosts(user.id, function(err, posts) { if (err) { callback(err); return; } fetchComments(posts[0].id, function(err, comments) { if (err) { callback(err); return; } callback(null, { user, posts, comments }); }); }); }); }
- コールバックを持つ関数をPromise化する方法
// コールバック関数をPromiseでラップする (Promisification) function fetchUser(userId) { return new Promise((resolve, reject) => { fetchUserCallback(userId, (err, user) => { if (err) reject(err); else resolve(user); }); }); } function fetchPosts(userId) { return new Promise((resolve, reject) => { fetchPostsCallback(userId, (err, posts) => { if (err) reject(err); else resolve(posts); }); }); } function fetchComments(postId) { return new Promise((resolve, reject) => { fetchCommentsCallback(postId, (err, comments) => { if (err) reject(err); else resolve(comments); }); }); }
Promise化した関数を使用してチェーンで記述すると、ソースコードが大幅に改善される。
// After: Promiseチェーン
// 処理の流れが直線的になり可読性が向上する
fetchUser(123)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log("コメント一覧:", comments))
.catch(error => console.error("処理に失敗した:", error.message));
Node.js環境では util.promisify 関数を使用することにより、エラーファーストコールバックスタイルの関数を自動的にPromise化できる。
// Node.jsのutil.promisifyを使用した自動Promise化
const { promisify } = require("util");
const fs = require("fs");
// fs.readFileはコールバックスタイルの関数
const readFileAsync = promisify(fs.readFile);
readFileAsync("./data.txt", "utf8")
.then(content => console.log(content))
.catch(error => console.error("読み込みに失敗した:", error.message));
Tauriでの使用例
Tauri v2の invoke() 関数は、JavaScriptからRustのコマンドを呼び出す時、常にPromiseを返す。
そのため、Tauriのフロントエンド開発においてPromiseは中心的な役割を担う。
- 基本
import { invoke } from '@tauri-apps/api/core'; // Rustコマンドを呼び出してPromiseチェーンで処理する invoke('greet', { name: 'World' }) .then(message => { document.getElementById('result').textContent = message; }) .catch(error => { console.error('コマンドの実行に失敗した:', error); });
- 対応するRust側のコマンド定義
#[tauri::command] async fn greet(name: &str) -> Result<String, String> { Ok(format!("Hello, {}!", name)) }
- 複数のコマンドをチェーンで連結する例
import { invoke } from '@tauri-apps/api/core'; // ファイルを読み込んで内容を処理するパイプライン invoke('read_file', { path: '/home/user/data.json' }) .then(content => { const data = JSON.parse(content); return invoke('process_data', { data }); }) .then(result => { return invoke('save_result', { result, path: '/home/user/output.json' }); }) .then(() => { console.log('処理が完了した'); }) .catch(error => { console.error('エラーが発生した:', error); }) .finally(() => { // ローディングスピナーを非表示にする等のクリーンアップ処理 setLoading(false); });
- 複数のRustコマンドを並列実行する例
import { invoke } from '@tauri-apps/api/core'; // Promise.allで複数のRustコマンドを並列実行する Promise.all([ invoke('get_system_info'), invoke('get_app_config'), invoke('get_user_preferences'), ]) .then(([systemInfo, config, preferences]) => { console.log('システム情報:', systemInfo); console.log('アプリ設定:', config); console.log('ユーザ設定:', preferences); }) .catch(error => { console.error('情報の取得に失敗した:', error); });
関連情報
- JavaScriptの基礎 - async await
- Promiseをより簡潔に記述するためのasync / await構文の詳細
- JavaScriptの基礎 - イベントループ
- JavaScriptの非同期処理を支えるイベントループの仕組み
- JavaScriptの基礎 - Fetch API
- Promiseを返すFetch APIによるHTTPリクエストの実装
- JavaScriptの基礎 - コールバック関数
- Promiseが解決した課題の背景となるコールバックパターンの詳細