順番に読み進めながら学べます

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 に渡した関数は最後に動く
delay(100, …)を呼ぶpending のPromise が返るseats.then(…)に関数を渡す渡した関数はまだ呼ばれない次の行のconsole.log問い合わせ中の文言が先に出るdelay を呼んでから 100ms 後fulfilled になり渡した関数が動く
then は関数を受け取っておくだけで、その場では呼びません。渡した関数は、下の行を実行し終えて fulfilled になってから動きます

届いた結果を使う処理を then の外に書くと、結果が決まる前に実行されるので値を使えません。結果を使う行は then に渡す関数の中にまとめ、待つ間に済ませたい表示は then の外の下の行に書きます。

最後に待つ行が無いと then の表示は出ない

この実行環境は、最後の行を終えるまでに出た表示だけを受け取ります。ブラウザや Node.js では待たなくても then の関数は動きますが、ここでは最後に await delay(1000); を置いて 1 秒待ちます。await(Promise の結果が決まるまで次の行へ進まない書き方)は次の記事で扱います。

動画 2 本の変換完了を通知します。delay は宣言済みです。

① convertVideo で、ms ミリ秒後に「intro.mp4 の変換が終わりました」の形で fulfilled になる Promise を返してください。

② intro.mp4 を 150ms、outro.mp4 を 80ms で変換し、届いた文言を表示してください。

③ ② より下で「変換中も編集を続けられます」と表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

失敗を受け取る — 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("入力欄を空にしました"));
  // 入力欄を空にしました(成功でも失敗でも最後に出る)
resolve と reject で決まる状態
new Promise で作る(pending)resolve に値を渡すreject にError を渡すresolve もreject も呼ばないfulfilledになるrejectedになるpending のままthen → finallyの順に呼ぶcatch → finallyの順に呼ぶthen も catch もfinally も呼ばない
左の 2 列では、then か catch の後に finally も呼ばれます。呼ばれる関数は、resolve か reject かで決まります

rejected になったときは、catch より前につないだ then の関数が飛ばされ、後ろの catch の関数が呼ばれます。この catch は Promise のメソッドで、try / catch 文とは別のものです。resolvereject も呼び忘れると、表示もエラーも出ないまま止まって見えます。

ギフトカードの残高を照会します。delay、balances、codes は宣言済みです。

① checkGiftCard で、「GIFT-」で始まらないコードは待たずに「コードの形式が違います」の Error で失敗させてください。

② それ以外は 100ms 後に「残高 3000 円」の形で成功させてください。

③ codes を順に照会し、結果の文言を表示してください。

④ 成功でも失敗でも最後に「照会中の表示を消しました」と表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

処理を順番につなぐ — 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 しないと then は待たない
returnsendMail(orderId)送信の結果が決まるまで待つresult に送信結果の文言が入る送ったあとで完了と表示sendMail(orderId)を呼ぶだけ待たずに次のthen が呼ばれるresult はundefined送り終える前に完了と表示
上の段だけが、送信の結果を待ってから次の関数を呼びます。Promise を return したときだけ、次の then は結果を待ちます

return を書かないと戻り値は undefined なので、次の then は送信を待たずに呼ばれ、result には undefined が入ります。エラーは出ないまま送信の結果だけが失われるので、気づきにくい誤りです。

波括弧を付けたら return は省けない

.then((id) => sendMail(id)) のように波括弧を付けないアロー関数は、sendMail が返した Promise をそのまま返すので、次の then は待ちます。途中にログを足そうとして本体を波括弧で囲んだときは、return sendMail(id);return を書き足します。

海外のショップで 12 ドルの商品を円で決済します。delay、dollars、loadRate、chargeCard は宣言済みです。

① レートで dollars を円に換算して「換算額: 1800 円」と表示し、金額を次へ渡してください。

② その金額で決済し、結果が決まってから次へ進むようにしてください。

③ 決済の結果を表示してください。

④ 途中で失敗したら「決済できません: 」に続けて Error の文言を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

複数の結果をまとめて待つ — 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));
渡した順と終わる順は一致しない
0 番目loadSales()100ms で 1840003 番目に終わる1 番目loadOrders()50ms で 421 番目に終わる2 番目loadInquiries()80ms で 32 番目に終わる
3 つは 50ms・80ms・100ms の順に終わります。then が受け取る [184000, 42, 3] は、左の渡した順に並びます

Promise.all 自体は問い合わせを始めず、そろうのを待つだけです。下の表は、渡した Promise の状態ごとに Promise.all が返す Promise と呼ばれる関数をまとめたものです。

渡した Promise の状態Promise.all が返す Promise呼ばれる関数と受け取る値
すべて fulfilled最後の 1 つが決まった時点で fulfilledthen が渡した順の配列を受け取る
1 つが rejectedその 1 つが rejected になった時点で rejectedcatch がその Error を受け取る
2 つ以上が rejected最初に rejected になった時点で rejectedcatch は最初の Error だけを受け取る
ほかは fulfilled で 1 つが pending のままpending のままthen も catch も呼ばれない

旅行の料金をまとめて見積もります。quote で始まる 4 つの関数は宣言済みです。

① estimateTrip で、受け取った Promise がそろうのを待ってください。

② 料金の合計を「合計: 68500 円」と表示してください。

③ 1 つでも失敗したら「見積もりできません: 」と Error の文言を表示してください。

④ 航空券・ホテル・レンタカーで 1 回、レンタカーを満車の quoteFullCar に替えて再度見積もってください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1delay(100, "A").then((v) => console.log(v)); の次の行が console.log("B"); のとき、表示の順は?

Q2rejected になる Promise に .then(A).catch(B).finally(C) とつなぐと、呼ばれるのは?

Q3Promise.all([delay(100, "A"), delay(50, "B")])then が受け取る配列は?