はじめに#
JavaScriptで非同期処理を扱うとき、避けて通れないのが「Promise(プロミス)」です。APIからデータを取得する、ファイルを読み込む、一定時間後に処理を実行するなど、多くの場面でPromiseは活躍します。
かつてはコールバック関数を使った非同期処理が主流でしたが、処理が複雑になると「コールバック地獄」と呼ばれる読みづらいコードに陥りがちでした。Promiseはこの問題を解決し、非同期処理を直感的に書けるようにする仕組みです。
本記事では、以下の内容を初心者向けにわかりやすく解説します。
- Promiseとは何か
- Promiseの3つの状態
resolveとrejectの使い方
then・catch・finallyによるチェーン処理
- Promiseを使う際の注意点
- 実践的な活用例
Promiseとは#
Promiseの基本概念#
Promiseとは、非同期処理の結果(成功または失敗)を表すオブジェクトです。「将来のある時点で値を返すことを約束する」というイメージから、Promise(約束)という名前が付けられています。
1
2
3
4
5
6
|
// Promiseの基本的な構造
const promise = new Promise((resolve, reject) => {
// 非同期処理を実行
// 成功した場合: resolve(値)
// 失敗した場合: reject(エラー)
});
|
Promiseを使うと、非同期処理の完了を待ってから次の処理を実行したり、エラーが発生した場合に適切に対処したりできます。
なぜPromiseが必要なのか#
JavaScriptはシングルスレッドで動作するため、時間のかかる処理を同期的に実行すると、その間他の処理がブロックされてしまいます。非同期処理を使えば、時間のかかる処理を待っている間も他の処理を続けられます。
従来のコールバック方式では、複数の非同期処理を連続して実行すると、ネストが深くなり可読性が低下していました。
1
2
3
4
5
6
7
8
9
10
|
// コールバック地獄の例(アンチパターン)
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
getYetMoreData(c, function(d) {
console.log(d);
});
});
});
});
|
Promiseを使えば、この問題を解決できます。
1
2
3
4
5
6
|
// Promiseを使った書き方
getData()
.then((a) => getMoreData(a))
.then((b) => getEvenMoreData(b))
.then((c) => getYetMoreData(c))
.then((d) => console.log(d));
|
Promiseの3つの状態#
Promiseは、作成されてから完了するまでに3つの状態を持ちます。
| 状態 |
英語名 |
説明 |
| 待機中 |
pending |
初期状態。まだ成功も失敗もしていない |
| 履行済み |
fulfilled |
処理が成功して完了した |
| 拒否済み |
rejected |
処理が失敗した |
stateDiagram-v2
[*] --> pending: Promise生成
pending --> fulfilled: resolve()を呼び出し
pending --> rejected: reject()を呼び出し
fulfilled --> [*]
rejected --> [*]一度「履行済み」または「拒否済み」になったPromiseは、その後状態が変わることはありません。この「履行済み」と「拒否済み」を合わせて「決定済み(settled)」と呼びます。
1
2
3
4
|
const promise = new Promise((resolve, reject) => {
resolve("成功"); // 履行済み(fulfilled)になる
reject("失敗"); // この行は無視される(すでに決定済みのため)
});
|
Promiseの基本構文#
Promiseの作成#
Promiseはnew Promise()コンストラクターを使って作成します。コンストラクターには「実行関数(executor)」と呼ばれる関数を渡します。
1
2
3
|
const myPromise = new Promise((resolve, reject) => {
// 非同期処理をここに書く
});
|
実行関数は2つの引数を受け取ります。
| 引数 |
説明 |
resolve |
処理が成功したときに呼び出す関数。引数に成功時の値を渡す |
reject |
処理が失敗したときに呼び出す関数。引数にエラーを渡す |
resolveとrejectの使い方#
resolveは処理の成功を、rejectは処理の失敗を表します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 成功する例
const successPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("処理が完了しました");
}, 1000);
});
// 失敗する例
const failPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("処理に失敗しました"));
}, 1000);
});
|
rejectにはErrorオブジェクトを渡すことが推奨されます。エラーオブジェクトにはスタックトレースが含まれるため、デバッグがしやすくなります。
実践的な例:ランダムに成功・失敗するPromise#
1
2
3
4
5
6
7
8
9
10
11
12
|
function randomOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve(`成功!乱数: ${random}`);
} else {
reject(new Error(`失敗!乱数: ${random}`));
}
}, 1000);
});
}
|
then・catch・finallyの使い方#
then()メソッド#
then()メソッドは、Promiseが履行されたときに実行するコールバック関数を登録します。
1
2
3
4
5
6
7
8
9
|
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve("Hello, Promise!");
}, 1000);
});
promise.then((value) => {
console.log(value); // "Hello, Promise!"
});
|
then()は2つの引数を受け取ることもできます。
1
2
3
4
5
6
7
8
9
10
|
promise.then(
(value) => {
// 成功時の処理
console.log("成功:", value);
},
(error) => {
// 失敗時の処理
console.log("失敗:", error);
}
);
|
ただし、エラー処理には後述するcatch()を使う方が一般的です。
catch()メソッド#
catch()メソッドは、Promiseが拒否されたときに実行するコールバック関数を登録します。
1
2
3
4
5
6
7
8
9
|
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("エラーが発生しました"));
}, 1000);
});
promise.catch((error) => {
console.error("エラーをキャッチ:", error.message);
});
|
catch()はthen(null, onRejected)の省略形です。チェーンの最後にcatch()を配置することで、チェーン内のどこでエラーが発生しても捕捉できます。
finally()メソッド#
finally()メソッドは、Promiseの結果(成功・失敗)に関わらず、最後に必ず実行する処理を登録します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("完了");
}, 1000);
});
promise
.then((value) => {
console.log("成功:", value);
})
.catch((error) => {
console.error("失敗:", error);
})
.finally(() => {
console.log("処理終了"); // 成功でも失敗でも実行される
});
|
finally()は、ローディング表示の終了やリソースのクリーンアップなど、結果に関係なく実行したい処理に適しています。
3つのメソッドを組み合わせた例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
console.log("データを取得中...");
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: "太郎", age: 25 });
} else {
reject(new Error("無効なユーザーIDです"));
}
}, 1500);
});
}
fetchUserData(1)
.then((user) => {
console.log("ユーザー情報:", user);
})
.catch((error) => {
console.error("エラー:", error.message);
})
.finally(() => {
console.log("データ取得処理が完了しました");
});
// 出力:
// データを取得中...
// ユーザー情報: { id: 1, name: '太郎', age: 25 }
// データ取得処理が完了しました
|
Promiseチェーン#
チェーンの基本#
then()メソッドは新しいPromiseを返すため、複数のthen()を連結(チェーン)できます。これを「Promiseチェーン」と呼びます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
new Promise((resolve) => {
resolve(1);
})
.then((value) => {
console.log(value); // 1
return value + 1;
})
.then((value) => {
console.log(value); // 2
return value + 1;
})
.then((value) => {
console.log(value); // 3
});
|
各then()のコールバックでreturnした値が、次のthen()の引数として渡されます。
Promiseを返すチェーン#
then()のコールバックでPromiseを返すと、そのPromiseが解決されるまで次のthen()は待機します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
wait(1000)
.then(() => {
console.log("1秒経過");
return wait(1000);
})
.then(() => {
console.log("2秒経過");
return wait(1000);
})
.then(() => {
console.log("3秒経過");
});
|
実践的なチェーンの例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
function getUser(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: "太郎" });
}, 500);
});
}
function getOrders(user) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, product: "商品A", userId: user.id },
{ id: 2, product: "商品B", userId: user.id },
]);
}, 500);
});
}
function getOrderDetails(orders) {
return new Promise((resolve) => {
setTimeout(() => {
const details = orders.map((order) => ({
...order,
price: Math.floor(Math.random() * 10000),
}));
resolve(details);
}, 500);
});
}
getUser(1)
.then((user) => {
console.log("ユーザー取得:", user.name);
return getOrders(user);
})
.then((orders) => {
console.log("注文数:", orders.length);
return getOrderDetails(orders);
})
.then((details) => {
console.log("注文詳細:", details);
})
.catch((error) => {
console.error("エラー:", error.message);
});
|
Promiseを使う際の注意点#
注意点1:returnを忘れない#
then()のコールバック内でPromiseを返すときは、必ずreturnを付けましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 悪い例:returnを忘れている
doSomething()
.then((result) => {
fetchData(result); // returnがないため、次のthenはundefinedを受け取る
})
.then((data) => {
console.log(data); // undefined
});
// 良い例:returnを付けている
doSomething()
.then((result) => {
return fetchData(result);
})
.then((data) => {
console.log(data); // fetchDataの結果
});
|
注意点2:エラーハンドリングを忘れない#
Promiseチェーンには必ずcatch()を付けて、エラーを適切に処理しましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 悪い例:catchがない
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult));
// エラーが発生しても、どこにも通知されない(未処理の拒否)
// 良い例:catchを付けている
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.catch((error) => {
console.error("エラーが発生しました:", error);
});
|
注意点3:Promiseコンストラクター内での例外#
new Promise()の実行関数内で例外が発生すると、自動的にPromiseが拒否されます。
1
2
3
4
5
6
7
|
const promise = new Promise((resolve, reject) => {
throw new Error("例外が発生!");
});
promise.catch((error) => {
console.log("キャッチ:", error.message); // "例外が発生!"
});
|
注意点4:thenのコールバック内での例外#
then()のコールバック内で例外が発生した場合も、後続のcatch()で捕捉できます。
1
2
3
4
5
6
7
|
Promise.resolve("OK")
.then((value) => {
throw new Error("thenの中で例外!");
})
.catch((error) => {
console.log("キャッチ:", error.message); // "thenの中で例外!"
});
|
注意点5:Promise.resolve()とPromise.reject()#
すでに決定済みのPromiseを作成するには、静的メソッドを使います。
1
2
3
4
5
6
7
8
|
// 履行済みのPromiseを作成
const fulfilled = Promise.resolve("成功した値");
// 拒否済みのPromiseを作成
const rejected = Promise.reject(new Error("失敗の理由"));
fulfilled.then((value) => console.log(value)); // "成功した値"
rejected.catch((error) => console.log(error.message)); // "失敗の理由"
|
複数のPromiseを扱う#
複数の非同期処理を同時に実行したい場合、Promiseの静的メソッドを使います。
Promise.all()#
すべてのPromiseが成功したときに履行され、1つでも失敗すると即座に拒否されます。
1
2
3
4
5
6
7
8
9
10
11
|
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // [1, 2, 3]
})
.catch((error) => {
console.error("いずれかが失敗:", error);
});
|
Promise.allSettled()#
すべてのPromiseが決定(成功または失敗)するまで待ちます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
const promise1 = Promise.resolve("成功");
const promise2 = Promise.reject(new Error("失敗"));
const promise3 = Promise.resolve("成功2");
Promise.allSettled([promise1, promise2, promise3])
.then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("成功:", result.value);
} else {
console.log("失敗:", result.reason.message);
}
});
});
// 出力:
// 成功: 成功
// 失敗: 失敗
// 成功: 成功2
|
Promise.race()#
最初に決定したPromiseの結果を返します。
1
2
3
4
5
6
7
|
const slow = new Promise((resolve) => setTimeout(() => resolve("遅い"), 2000));
const fast = new Promise((resolve) => setTimeout(() => resolve("速い"), 500));
Promise.race([slow, fast])
.then((value) => {
console.log(value); // "速い"
});
|
Promise.any()#
最初に成功したPromiseの結果を返します。すべて失敗した場合のみ拒否されます。
1
2
3
4
5
6
7
8
9
10
11
|
const fail1 = Promise.reject(new Error("失敗1"));
const fail2 = Promise.reject(new Error("失敗2"));
const success = Promise.resolve("成功");
Promise.any([fail1, fail2, success])
.then((value) => {
console.log(value); // "成功"
})
.catch((error) => {
console.log("すべて失敗:", error);
});
|
| メソッド |
履行条件 |
拒否条件 |
Promise.all() |
すべて成功 |
1つでも失敗 |
Promise.allSettled() |
すべて決定後、常に履行 |
なし |
Promise.race() |
最初に決定したものに従う |
最初に決定したものに従う |
Promise.any() |
最初に成功 |
すべて失敗 |
実践例:APIデータの取得#
Promiseを使った実践的な例として、fetch APIでデータを取得する処理を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function fetchTodo(id) {
return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
return response.json();
});
}
fetchTodo(1)
.then((todo) => {
console.log("取得したTodo:", todo);
console.log("タイトル:", todo.title);
})
.catch((error) => {
console.error("取得に失敗:", error.message);
})
.finally(() => {
console.log("API呼び出し完了");
});
|
fetch()自体がPromiseを返すため、そのままthen()でチェーンできます。response.json()もPromiseを返すので、ここでもreturnが必要です。
async/awaitとの関係#
ES2017で導入されたasync/await構文を使うと、Promiseをより直感的に扱えます。async/awaitはPromiseの上に構築された構文糖衣です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// Promiseチェーンでの書き方
function fetchDataWithPromise() {
return getUser(1)
.then((user) => getOrders(user))
.then((orders) => getOrderDetails(orders))
.then((details) => {
console.log(details);
return details;
})
.catch((error) => {
console.error(error);
});
}
// async/awaitでの書き方
async function fetchDataWithAsync() {
try {
const user = await getUser(1);
const orders = await getOrders(user);
const details = await getOrderDetails(orders);
console.log(details);
return details;
} catch (error) {
console.error(error);
}
}
|
async/awaitを使うと、同期的なコードのように書けるため、可読性が向上します。ただし、Promiseの仕組みを理解していることが前提となります。
まとめ#
本記事では、JavaScriptのPromiseについて基本から実践的な使い方まで解説しました。
- Promiseは非同期処理の結果を表すオブジェクトで、「待機中」「履行済み」「拒否済み」の3つの状態を持つ
resolveで成功、rejectで失敗を表す
then()で成功時、catch()で失敗時、finally()で常に実行する処理を登録する
then()は新しいPromiseを返すため、チェーンで連結できる
- 複数のPromiseを扱うには
Promise.all()、Promise.allSettled()、Promise.race()、Promise.any()を使う
async/awaitはPromiseをより直感的に書くための構文
Promiseを理解することで、API通信やタイマー処理など、非同期処理を伴うアプリケーション開発がスムーズになります。async/await構文もPromiseがベースになっているため、まずはPromiseの基本をしっかり押さえておきましょう。
参考リンク#