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

async / await — Promise の結果を上の行から順に待つ

Promise の結果を待ってから次の行へ進む async / await を扱います。try / catch での失敗の受け止め方、直列と Promise.all の待ち時間の違い、await を忘れたときに起きることまで確かめます。

Promise の then では、届いた値を関数の引数でしか受け取れません。1 つ目の結果を 3 つ目の処理で使うには関数を入れ子にするか外の変数に移す必要があり、読みにくくなります。

この記事では、結果を待って次の行へ進む async / await と、複数の Promise の待ち方を扱います。

上の行から順に書く — async 関数と await

駐車場の空き区画を押さえ、割り当てられた区画の番号を案内文に入れるとします。then で書くと、区画の番号を使う処理はすべて then に渡す関数の中に置くことになり、その外の下の行に書いた処理は番号が届く前に実行されます。

async 関数async を付けて宣言し、呼ぶと必ず Promise を返す関数)の中では await が使えます。await は結果が決まるまでその関数の続きだけを止め、fulfilled の値を取り出します。止まっている間、呼び出し側には pending の Promise が先に返ります。

// ms ミリ秒後に value で fulfilled になる Promise を返す(前の記事と同じ)
function delay(ms, value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}
const assignSpot = () => delay(100, "B2-17");

// async を付けた関数の中で await を書く
async function showParking() {
  console.log("空き区画を探しています");      // 空き区画を探しています
  const spot = await assignSpot();            // fulfilled になるまで次の行へ進まない
  console.log(`${spot} 区画を押さえました`);  // B2-17 区画を押さえました
  return `${spot} 区画に駐車できます`;        // 返してある Promise が、この文言で fulfilled になる
}

// 呼び出し側も、返ってきた Promise を await で待つ
const message = await showParking();
console.log(message);                         // B2-17 区画に駐車できます
2 つの await がそれぞれ待つもの
呼び出し側 — const message = await showParking()
  • showParking() は中の最初の await まで進むと、pending の Promise を返す
  • その Promise が fulfilled になるまで console.log(message) へ進まない
async function showParking() の中
  • 空き区画を探しています を表示する
  • await assignSpot() — 100ms 後に fulfilled になるまで止まる
  • spot"B2-17" が入ってから次の行へ進む
  • return した文言で、返した Promise が fulfilled になる
内側は assignSpot の結果を、外側は関数の完了を待ちます。return した値が message に入ります

return を書かない async 関数も、本体の最後まで進むと undefined で fulfilled になる Promise を返します。値を受け取らないときも、呼び出しに await を付ければ、中の処理が終わってから次の行へ進めます。

関数の外に書く await は場所が限られる

このページの実行環境はコード全体を async 関数で包むので、const message = await showParking(); のように関数の外にも await を書けます。ふつうの JavaScript ファイルでは、async 関数の外に書いた await が SyntaxError になることがあります。このページ以外で書くときは、await を async 関数の中に置きます。

美容院の予約通知を出します。delay、confirmBooking、registerReminder は宣言済みです。

① notifyBooking を、中で結果を待てる関数に書き換えてください。

② 確定日時を待ち、「ご予約日時: 9 月 14 日 15:00」と表示してください。

③ その日時で通知を登録し、結果を表示してください。

④ 外側で完了を待ち、「予約の手続きが終わりました」と表示してください。

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

JavaScript / TypeScript エディタ

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

失敗を例外として扱う — await と try / catch

チケットの座席を確保してからカードで支払う処理では、支払いが失敗することもあります。await で値を取り出す書き方ではチェーンを作らないので、前の記事のように失敗を末尾の .catch で受け取る形になりません。

async 関数の中で throw すると、その関数が返す Promise は rejected になります。rejected の Promise を await すると、その await の行で同じ Error が投げられるので、try / catch でそのまま囲めます。

// delay は最初の章のコードと同じ
const holdSeat = () => delay(100, "S-204");
const sendReceipt = (seatId) => delay(100, `${seatId}の領収書を送りました`);

// async 関数の中で throw すると、返す Promise は rejected になる
async function payByCard() {
  await delay(100);                          // 値は使わず、100ms 待つだけ
  throw new Error("カードの有効期限が切れています");
}

try {
  const seatId = await holdSeat();
  console.log(`${seatId}を確保しました`);           // S-204を確保しました
  await payByCard();                                // rejected なので、この行で例外になる
  const receipt = await sendReceipt(seatId);        // ここから下は実行されない
  console.log(receipt);
} catch (error) {
  console.log(`購入できません: ${error.message}`);  // 購入できません: カードの有効期限が切れています
}
成功と失敗で次に動く行
支払いが成功した場合await payByCard()が終わる次の行のsendReceipt へ領収書を送る有効期限が切れていた場合await payByCard()の行で例外try の残りを飛ばすcatch (error)で文言を表示
上の段は次の行へ進み、下の段は try の残りを飛ばして catch へ移ります。進む先は await した Promise の状態で決まります

catch が受け取るのは try の波括弧の中で起きた例外だけなので、失敗するかもしれない await の行は try の中に書きます。成功でも失敗でも行う後片付けは、try / catch 文の finally にそのまま書けます。

本の返却期限の延長を申し込みます。delay、loans、renewLoan は宣言済みです。

① requestRenewal で renewLoan の結果を待ち、受け取った文言を表示してください。

② 失敗したら、「延長できません: 」に続けて Error の文言を表示してください。

③ 成功でも失敗でも最後に「統計の基礎の手続きを終えました」の形で表示してください。

④ loans の 1 冊目の完了を待ってから、2 冊目を申し込んでください。

JavaScript / TypeScript エディタ

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

3 つを同時に始めて待つ — 直列と Promise.all

商品ページを開くときに、在庫数・レビュー件数・お届け予定日を問い合わせるとします。await を 1 行ずつ並べると、どの行も前の結果を使わないのに、前が届くまで次の問い合わせを始めません。関係の無い問い合わせまで順番待ちになります。

前が終わってから次を始める進め方を 直列、複数を同時に始めてから待つ進め方を 並列 と呼びます。並列にするには 3 つの関数を先に呼んで Promise を受け取り、前の記事の Promise.all に渡して、then の代わりに await で待ちます。

// delay は最初の章のコードと同じ
const loadStock = () => delay(120, 8);
const loadReviews = () => delay(50, 23);
const loadArrival = () => delay(90, "9 月 13 日");

// 直列: 1 つ届いてから次を呼ぶ(そろうまで 120 + 50 + 90 = 260ms)
const stock = await loadStock();
const reviews = await loadReviews();
const arrival = await loadArrival();
console.log([stock, reviews, arrival].join(" / "));  // 8 / 23 / 9 月 13 日

// 並列: 先に 3 つとも呼んでから待つ(そろうまで最も遅い 120ms)
const promises = [loadStock(), loadReviews(), loadArrival()];
const results = await Promise.all(promises);
console.log(results.join(" / "));                   // 8 / 23 / 9 月 13 日(結果は同じ)
直列と並列で結果が届く順番
直列 0ms在庫を呼ぶ120ms 在庫が届くレビューを呼ぶ170ms レビューが届く予定日を呼ぶ260ms3 つそろう並列 0ms3 つとも呼ぶ50msレビューが届く90ms予定日が届く120ms 在庫が届く3 つそろう
下の段は 0ms に 3 つとも始まり、最も遅い 120ms でそろいます。await を 1 行ずつ並べると、待ち時間は合計の 260ms です

for...of の本体に await を書くと、配列の要素も 1 件ずつ直列に進み、待ち時間は要素ごとの合計です。下の表は、直列と並列のどちらを選ぶかを場面ごとにまとめたものです。

場面書き方理由
在庫数・レビュー件数・お届け予定日を読み込むPromise.all を await する(並列)互いの結果を使わないので、同時に始められる
注文番号を受け取ってから確認メールを送るawait を 1 行ずつ並べる(直列)2 つ目の呼び出しに 1 つ目の結果が要る
配列の要素を 1 件ずつ順に処理するfor...of の本体で await する(直列)並列にすると、要素の処理が終わる順が配列の順とずれる

天気アプリで 3 都市の予報を読み込み、届く順番を比べます。delay、loadForecast、cities は宣言済みです。

① cities の順に、1 都市の予報が届いてから次の都市を読み込んでください。

② 「1 都市ずつ読み込みました」と表示してください。

③ 今度は 3 都市を同時に読み込み始め、すべてそろうのを待ってください。

④ そろった予報を「 / 」でつないで表示してください。

JavaScript / TypeScript エディタ

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

await を書かずに受け取る — 変数に入る Promise

会員のポイント残高を画面に出し、1000 ポイント以上なら特典を案内するとします。async 関数を呼ぶときに await を付け忘れても SyntaxError にはならず、表示と判定の結果が変わるだけなので、どこで間違えたのかに気づきにくくなります。

async 関数が返すのは return した値ではなく、その値で fulfilled になる Promise です。await を付けずに受け取ると変数には Promise が入り、テンプレートリテラルに埋め込むと [object Promise](Promise を文字列にした表記)と表示されます。

// delay は最初の章のコードと同じ
async function loadPoints() {
  await delay(100);
  return 1200;                              // 返してある Promise が 1200 で fulfilled になる
}

// await を付け忘れると、points には Promise が入る
const points = loadPoints();
console.log(`保有ポイント: ${points}`);      // 保有ポイント: [object Promise]
console.log(points >= 1000);                // false(Promise を数値にすると NaN なので成り立たない)

// await を付けると、fulfilled の値が入る
const balance = await loadPoints();
console.log(`保有ポイント: ${balance}`);     // 保有ポイント: 1200
console.log(balance >= 1000);               // true
points は Promise のまま残る
points =loadPoints()pending のPromise が入る100ms 後にfulfilled になるpoints の中身はPromise のまま文字列に埋め込む[object Promise]と表示される
fulfilled になっても、points が 1200 に置き換わることはありません。値を受け取るには await が要ります

表示が [object Promise] になる、成り立つはずの条件が false になる、のどちらかが起きたら、async 関数の呼び出しに await が付いているかを確かめます。if の条件に置いた場合は、Promise はオブジェクトなので truthy で、返す値に関係なく成り立ちます。

await を忘れた失敗は catch に届かない

try の中で、rejected の Promise を返す async 関数を await を付けずに呼ぶと、その行では例外が起きず、catch は実行されません。失敗はどこにも受け取られず、このページの実行環境では何も表示されません。catch で受け取るには、呼び出しに await を付けます。

クーポンの有効期限を確かめて、使えるかどうかを案内します。delay、coupons、isExpired は宣言済みです。

① await を付けずに、1 枚目のクーポンで isExpired を呼んだ結果を if の条件にしてください。

② 成り立てば「AUTUMN10 は期限切れです」、成り立たなければ「AUTUMN10 を使えます」の形で表示してください。

③ coupons を順に回し、結果を待ってから同じ判定をして表示してください。

JavaScript / TypeScript エディタ

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

理解度チェック

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

Q1try の中で、rejected になる Promise を await した行より下の行はどうなりますか?

Q270ms・30ms・50ms で届く 3 つの呼び出しを await で 1 行ずつ待つと、そろうまで約何 ms ですか?

Q3return 1200; で終わる async 関数を await せずに呼んで変数へ入れると、変数に入るのは?