Q1delay(100, "A").then((v) => console.log(v)); の次の行が console.log("B"); のとき、表示の順は?
Promise — 非同期の結果を then と catch で受け取る
時間のかかる処理の結果をあとから受け取る Promise を扱います。3 つの状態と then / catch / finally、then のチェーン、Promise.all での同時実行まで確かめます。
注文の送信や在庫の問い合わせのように、結果が届くまで時間がかかる処理があります。待つ間に画面が固まらないよう、こうした処理は結果を待たずに次の行へ進み、結果をあとで受け取る形で書きます。この書き方を 非同期処理 と呼びます。
この記事では、あとで決まる結果を表す Promise と、複数の Promise をまとめて待つ Promise.all を扱います。
結果をあとから受け取る — Promise と then
座席の空き状況の答えが、問い合わせてから 100ms 後に届くとします。setTimeout(指定したミリ秒が経ってから関数を 1 回呼ぶ関数)は呼んだ直後に次の行へ進むので、100ms 後の答えを関数の戻り値として return することはできません。
答えがあとで入る Promise(あとで決まる結果を表すオブジェクト)を返します。new Promise((resolve) => { ... }) に渡した関数はその場ですぐ呼ばれ、中で resolve(値) を呼ぶと pending(結果待ち)から fulfilled(成功)になります。then メソッドに渡した関数は、fulfilled になったときにその値で呼ばれます。
// ms ミリ秒後に value で fulfilled になる Promise を返す
function delay(ms, value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), ms);
});
}
// 問い合わせを始める(この時点では pending)
const seats = delay(100, "残り 12 席");
// fulfilled になったときに呼ぶ関数を渡しておく
seats.then((message) => {
console.log(message); // 残り 12 席(2 番目に表示)
});
console.log("空き状況を問い合わせています"); // 空き状況を問い合わせています(1 番目に表示)
then は関数を受け取っておくだけで、その場では呼びません。渡した関数は、下の行を実行し終えて fulfilled になってから動きます。届いた結果を使う処理を then の外に書くと、結果が決まる前に実行されるので値を使えません。結果を使う行は then に渡す関数の中にまとめ、待つ間に済ませたい表示は then の外の下の行に書きます。
最後に待つ行が無いと then の表示は出ない
この実行環境は、最後の行を終えるまでに出た表示だけを受け取ります。ブラウザや Node.js では待たなくても then の関数は動きますが、ここでは最後に await delay(1000); を置いて 1 秒待ちます。await(Promise の結果が決まるまで次の行へ進まない書き方)は次の記事で扱います。
失敗を受け取る — reject / catch / finally
カードの暗証番号の照合は、番号が違って失敗することもあります。resolve しか使えないと失敗も成功と同じ値として届くため、受け取った関数の中で成功か失敗かを毎回 if で見分けることになります。
new Promise に渡す関数は、2 番目の引数で reject も受け取れます。reject(new Error("文言")) のように呼ぶと rejected(失敗)になります。catch に渡した関数は rejected のときにその Error を受け取り、finally に渡した関数は成功でも失敗でも最後に呼ばれます。
function verifyPin(pin) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (pin === "4821") {
resolve("照合できました");
} else {
// 失敗は Error を reject に渡して知らせる
reject(new Error("暗証番号が違います"));
}
}, 100);
});
}
// then・catch・finally は続けてつなげて書ける(仕組みは次の章)
verifyPin("0000")
.then((message) => console.log(message)) // rejected なので呼ばれない
.catch((error) => console.log(error.message)) // 暗証番号が違います
.finally(() => console.log("入力欄を空にしました"));
// 入力欄を空にしました(成功でも失敗でも最後に出る)
rejected になったときは、catch より前につないだ then の関数が飛ばされ、後ろの catch の関数が呼ばれます。この catch は Promise のメソッドで、try / catch 文とは別のものです。resolve も reject も呼び忘れると、表示もエラーも出ないまま止まって見えます。
処理を順番につなぐ — then のチェーンと return
注文を確定したあとで、返ってきた注文番号を使って確認メールを送りたいとします。2 つ目の問い合わせを 1 つ目の then の中に書き、3 つ目をさらにその中に書くと、手順が増えるたびに字下げが深くなって読みにくくなります。
then は新しい Promise を返すので、.then(...).then(...) とつなげられます。これを チェーン(then が返した Promise に次の then をつなぐ書き方)と呼びます。then の関数で return した値は、次の then の関数が受け取ります。
// delay は最初の章のコードと同じ
const placeOrder = () => delay(100, "A-2046");
const sendMail = (orderId) => delay(100, `${orderId} に確認メールを送りました`);
placeOrder()
.then((orderId) => {
console.log(`注文番号: ${orderId}`); // 注文番号: A-2046
return sendMail(orderId); // 送信の結果が決まるまで次へ進まない
})
.then((result) => {
console.log(result); // A-2046 に確認メールを送りました
return "手続きが完了しました"; // 値はそのまま次の関数に渡る
})
.then((status) => console.log(status)); // 手続きが完了しました
return を書かないと戻り値は undefined なので、次の then は送信を待たずに呼ばれ、result には undefined が入ります。エラーは出ないまま送信の結果だけが失われるので、気づきにくい誤りです。
波括弧を付けたら return は省けない
.then((id) => sendMail(id)) のように波括弧を付けないアロー関数は、sendMail が返した Promise をそのまま返すので、次の then は待ちます。途中にログを足そうとして本体を波括弧で囲んだときは、return sendMail(id); と return を書き足します。
複数の結果をまとめて待つ — Promise.all
管理画面を開いたときに、今日の売上・注文件数・問い合わせ件数をそれぞれ問い合わせ、3 つそろってから集計欄を表示するとします。3 つの Promise に別々に then を付けると、どれが最後に届くかは毎回違うので、そろったことをどこで判定するかが決まりません。
Promise.all(Promise の配列を受け取り、全部の結果がそろうのを待つメソッド)に、関数を呼んで得た 3 つの Promise を渡すと、3 つの結果を配列で一度に受け取れます。1 つでも rejected になると、then ではなく catch へ進みます。
// delay は最初の章のコードと同じ
const loadSales = () => delay(100, 184000);
const loadOrders = () => delay(50, 42);
const loadInquiries = () => delay(80, 3);
// 関数を呼んだ時点で 3 つとも始まる。Promise.all はそろうのを待つ
Promise.all([loadSales(), loadOrders(), loadInquiries()])
// 結果の配列を、引数の位置で分割代入して受け取る
.then(([sales, orders, inquiries]) => {
// 終わった順ではなく、配列に書いた順で受け取る
console.log(`売上: ${sales} 円`); // 売上: 184000 円
console.log(`注文: ${orders} 件`); // 注文: 42 件
console.log(`問い合わせ: ${inquiries} 件`); // 問い合わせ: 3 件
})
.catch((error) => console.log(error.message));
Promise.all 自体は問い合わせを始めず、そろうのを待つだけです。下の表は、渡した Promise の状態ごとに Promise.all が返す Promise と呼ばれる関数をまとめたものです。
| 渡した Promise の状態 | Promise.all が返す Promise | 呼ばれる関数と受け取る値 |
|---|---|---|
| すべて fulfilled | 最後の 1 つが決まった時点で fulfilled | then が渡した順の配列を受け取る |
| 1 つが rejected | その 1 つが rejected になった時点で rejected | catch がその Error を受け取る |
| 2 つ以上が rejected | 最初に rejected になった時点で rejected | catch は最初の Error だけを受け取る |
| ほかは fulfilled で 1 つが pending のまま | pending のまま | then も catch も呼ばれない |
理解度チェック
まずは1問ずつ答えてみましょう。
Q2rejected になる Promise に .then(A).catch(B).finally(C) とつなぐと、呼ばれるのは?
Q3Promise.all([delay(100, "A"), delay(50, "B")]) の then が受け取る配列は?