Q1try の中で、rejected になる Promise を await した行より下の行はどうなりますか?
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 区画に駐車できます
showParking()は中の最初のawaitまで進むと、pending の Promise を返す- その Promise が fulfilled になるまで
console.log(message)へ進まない
空き区画を探していますを表示するawait assignSpot()— 100ms 後に fulfilled になるまで止まるspotに"B2-17"が入ってから次の行へ進むreturnした文言で、返した Promise が fulfilled になる
return を書かない async 関数も、本体の最後まで進むと undefined で fulfilled になる Promise を返します。値を受け取らないときも、呼び出しに await を付ければ、中の処理が終わってから次の行へ進めます。
関数の外に書く await は場所が限られる
このページの実行環境はコード全体を async 関数で包むので、const message = await showParking(); のように関数の外にも await を書けます。ふつうの JavaScript ファイルでは、async 関数の外に書いた await が SyntaxError になることがあります。このページ以外で書くときは、await を async 関数の中に置きます。
失敗を例外として扱う — 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}`); // 購入できません: カードの有効期限が切れています
}
catch が受け取るのは try の波括弧の中で起きた例外だけなので、失敗するかもしれない await の行は try の中に書きます。成功でも失敗でも行う後片付けは、try / catch 文の finally にそのまま書けます。
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 日(結果は同じ)
for...of の本体に await を書くと、配列の要素も 1 件ずつ直列に進み、待ち時間は要素ごとの合計です。下の表は、直列と並列のどちらを選ぶかを場面ごとにまとめたものです。
| 場面 | 書き方 | 理由 |
|---|---|---|
| 在庫数・レビュー件数・お届け予定日を読み込む | Promise.all を await する(並列) | 互いの結果を使わないので、同時に始められる |
| 注文番号を受け取ってから確認メールを送る | await を 1 行ずつ並べる(直列) | 2 つ目の呼び出しに 1 つ目の結果が要る |
| 配列の要素を 1 件ずつ順に処理する | for...of の本体で await する(直列) | 並列にすると、要素の処理が終わる順が配列の順とずれる |
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
表示が [object Promise] になる、成り立つはずの条件が false になる、のどちらかが起きたら、async 関数の呼び出しに await が付いているかを確かめます。if の条件に置いた場合は、Promise はオブジェクトなので truthy で、返す値に関係なく成り立ちます。
await を忘れた失敗は catch に届かない
try の中で、rejected の Promise を返す async 関数を await を付けずに呼ぶと、その行では例外が起きず、catch は実行されません。失敗はどこにも受け取られず、このページの実行環境では何も表示されません。catch で受け取るには、呼び出しに await を付けます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q270ms・30ms・50ms で届く 3 つの呼び出しを await で 1 行ずつ待つと、そろうまで約何 ms ですか?
Q3return 1200; で終わる async 関数を await せずに呼んで変数へ入れると、変数に入るのは?