文法まとめ — 配列メソッド・非同期・例外の早見表

配列メソッドを戻り値の形で、非同期処理をコールバック方式・then・await の 3 つの書き方で、例外を失敗が届く場所で見比べる早見表です。チェーンを続けられる条件もまとめます。

請求の一覧から合計を出す、アップロードの完了を待つ、失敗を受け取って案内を出すといった処理には、同じ目的に使える書き方が複数あります。選び方を誤ると、配列のつもりで undefined を受け取ったり、失敗がどの catch にも届かなかったりします。

この記事では、配列メソッド・非同期の待ち方・例外の受け取り方の使い分けを扱います。

欲しい結果の形から選ぶ — 配列メソッドの戻り値

請求の一覧から、支払い済みの件数、最初の未払いの 1 件、請求額の合計をそれぞれ出したいとします。配列メソッドはどれもコールバックを渡す同じ形で呼ぶので、見た目だけでは、配列が返るのか値が 1 つ返るのかを区別できません。

ここでは、メソッドが返す値を 戻り値の形(新しい配列・配列の中にあった要素そのもの・真偽値や合計のように配列から計算した値のどれか)で分けます。形が決まれば、返った値に続けて join を呼べるのか、.id のようにプロパティを読むのかも決まります。

const invoices = [
  { id: "B-301", amount: 4800, paid: true },
  { id: "B-302", amount: 1200, paid: false },
  { id: "B-303", amount: 3600, paid: true },
  { id: "B-304", amount: 2500, paid: false },
];

// 配列が返る: map は件数そのまま、filter は条件に合う分だけ
console.log(invoices.map((invoice) => invoice.id).join(", "));   // B-301, B-302, B-303, B-304
console.log(invoices.filter((invoice) => invoice.paid).length);  // 2

// 要素そのものが返る: 最初に条件に合った 1 件
console.log(invoices.find((invoice) => !invoice.paid).id);  // B-302

// 配列から計算した値が返る: some は真偽値、reduce は畳み込んだ値
console.log(invoices.some((invoice) => invoice.amount >= 4000));              // true
console.log(invoices.reduce((total, invoice) => total + invoice.amount, 0));  // 12100
同じ配列でも戻り値の形が変わる
invoices4 件の請求.filter でpaid を判定.find で!paid を判定.reduce でamount を足す配列(2 件)B-301 と B-303要素 1 つB-302 の請求数値 1 つ12100続けて.map や .join続けて.id や .amountそのままconsole.log へ
返る形は配列・要素・数値に分かれ、うしろに書けるものも変わります。欲しい結果の形を決めると、使うメソッドが絞れます

1 件だけ欲しいときに filter を使うと、返るのは 1 件入りの配列なので、.id を読む前に [0] を付けることになります。下の表は、このカテゴリで扱った、関数を渡す配列メソッドを戻り値の形で並べたものです。

メソッド返すもの注意
map新しい配列(件数は同じ)元の配列は変わらない
filter新しい配列(合う分だけ)0 件なら空の配列
find / findIndex要素 / 位置無いと undefined / -1
some / every真偽値空の配列では false / true
reduce最後の周が返した値空の配列は初期値が必要
sort / toSorted並べ替えた配列sort は元の配列も変わる

続けて呼べるかを見分ける — チェーンと forEach

未払いの請求額だけを合計したいとします。filter の結果を変数に入れ、その変数で map を呼び、さらに reduce を呼ぶと、あとで使わない途中の配列にも名前が増えます。1 つの式につなげたくても、どのメソッドのうしろなら続けられるかを知らないと、つないだ位置で TypeError になります。

チェーン(前のメソッドの戻り値に対して次のメソッドを呼ぶ書き方)では、.filter(...) のような 1 回の呼び出しを 1 段と数え、各段は 1 つ前の段が返した値に対して呼ばれます。配列メソッドを続けられるのは、前の段が配列を返したときだけです。

// invoices は前の章と同じ 4 件の請求

// 配列が返る間は、うしろに配列メソッドを続けられる
const unpaidTotal = invoices
  .filter((invoice) => !invoice.paid)             // 配列: B-302 と B-304 の 2 件
  .map((invoice) => invoice.amount)               // 配列: [1200, 2500]
  .reduce((total, amount) => total + amount, 0);  // 数値: 3700
console.log(unpaidTotal);                         // 3700

// toSorted も配列を返すので、map と join まで続けられる
const idsByAmount = invoices.toSorted((a, b) => b.amount - a.amount).map((invoice) => invoice.id);
console.log(idsByAmount.join(", "));  // B-301, B-303, B-304, B-302

// forEach が返すのは undefined なので、うしろに続けると止まる
invoices.forEach((invoice) => console.log(invoice.id)).map((invoice) => invoice.id);
// B-301 から B-304 までを表示したあと
// TypeError: Cannot read properties of undefined (reading 'map')
戻り値が配列の間だけつなげる
invoices4 件の配列.filter(...)2 件の配列.map(...)[1200, 2500].reduce(...)3700invoices4 件の配列.forEach(...)undefinedundefined の.map(...)TypeErrorで止まる
上の段は配列が返る間だけ次を呼べて、reduce の数値で終わります。forEach は undefined を返すので、次の段で止まります

配列を返さない段は、チェーンの最後に置きます。find のうしろでは .id のようにプロパティを読めても map は呼べず、reduce が返した数値はそのまま変数に入れて使います。

find の後ろで map を呼ぶと文言が変わる

find が返した要素や reduce が返した数値に .map(...) を続けると、TypeError: invoices.find(...).map is not a function のような TypeError で止まります。forEach の後ろとは文言が違いますが、どちらも .map の 1 つ前の段が配列を返していません。

待ち方を比べる — コールバック / then / await

写真をアップロードしてから、その縮小版を作るとします。どちらも結果が届くまで時間がかかるため、関数の戻り値として結果を返せず、2 つ目の手順は 1 つ目の結果が届いてから始める必要があります。

下のコードの ① は コールバック方式(処理を始める関数に、終わったら呼ぶ関数を引数で渡す書き方。Promise は返さない)で、setTimeout もこの形です。② は受け取った Promise の then に関数を渡す書き方、③ は async 関数の中で await を使う書き方です。

// ① コールバック方式: 終わったら呼ぶ関数を渡し、次の手順はその中に書く
// later は 100ms 後に value を渡して callback を呼ぶ(Promise は返さない)
function later(value, callback) {
  setTimeout(() => callback(value), 100);
}
later("photo-17.jpg", (fileName) => {
  later(`${fileName} の縮小版`, (thumbnail) => {
    console.log(`① ${thumbnail}を作りました`);  // ① photo-17.jpg の縮小版を作りました
  });
});

// ② then: 次の手順を .then でつなぐ(delay は Promise の記事と同じ)
delay(100, "photo-17.jpg")
  .then((fileName) => delay(100, `${fileName} の縮小版`))
  .then((thumbnail) => console.log(`② ${thumbnail}を作りました`));  // ② photo-17.jpg の縮小版を作りました

// ③ await: 次の手順を下の行に書き、結果は変数で受け取る(前の記事と同じく async 関数の中で動く前提)
const fileName = await delay(100, "photo-17.jpg");
const thumbnail = await delay(100, `${fileName} の縮小版`);
console.log(`③ ${thumbnail}を作りました`);  // ③ photo-17.jpg の縮小版を作りました
次の手順を書く場所の深さ
① コールバック方式 — later に関数を渡す
  • later("photo-17.jpg", (fileName) => { … }) を呼ぶ
(fileName) => { … } の中
  • 1 つ目の結果が fileName に届く
  • 2 つ目の later をここで呼ぶ
(thumbnail) => { … } の中
  • 2 つ目の結果が thumbnail に届く
  • 表示はいちばん内側に書く
② then — .then に関数を渡す
  • delay(100, "photo-17.jpg") から始める
1 つ目の .then に渡す関数の中
  • fileName を受け取り、次の delay を返す
2 つ目の .then に渡す関数の中
  • thumbnail を受け取って表示する
③ await — 結果を変数で受け取る
  • const fileName = await delay(…)
  • const thumbnail = await delay(…)
  • console.log(…) を次の行に書く
① のコールバック方式だけ、結果が届く関数の中に次の関数が入ります。③ の await は関数を作らず、上から順の行になります

2 つ目の then の関数には thumbnail しか届かないので、そこで fileName も使うには外の変数に移すことになります。await なら、変数に入った fileName を下のどの行からでも読めます。下の表は、3 つの書き方を並べたものです。

書き方結果を受け取る場所手順が増えたとき
コールバック方式渡した関数の引数1 段ずつ深くなる
then に関数を渡すthen の関数の引数同じ深さでつなぐ
await で待つ左辺の変数次の行に書く

失敗が届く先を確かめる — try / catch と .catch

在庫を確保する処理が、注文数が在庫を超えたときに失敗するとします。Promise を返さない普通の関数なら、呼んだ行を try / catch で囲めば受け取れますが、Promise を返す関数の失敗は、書き方によって catch 文に届いたり届かなかったりします。

try に続く catch 文は、囲んだ行で起きた例外を受け取ります。Promise の .catch は、その Promise が rejected になったときに渡した関数を呼びます。

function checkStock(count) {
  if (count > 3) {
    throw new Error(`在庫が足りません: ${count} 点`);
  }
}
async function reserveStock(count) {
  await delay(100);   // delay は Promise の記事と同じ
  checkStock(count);  // ここで throw すると、返す Promise は rejected になる
}

// ① Promise を返さない関数: 呼んだ行を try で囲む
try { checkStock(5); } catch (error) { console.log(`① ${error.message}`); }
// ② then の書き方: .catch に受け取る関数を渡す
reserveStock(5).catch((error) => console.log(`② ${error.message}`));
// ③ await の書き方: await した行を try で囲む
try { await reserveStock(5); } catch (error) { console.log(`③ ${error.message}`); }

// ① 在庫が足りません: 5 点
// ② 在庫が足りません: 5 点(100ms 後)
// ③ 在庫が足りません: 5 点(100ms 後)
同じ失敗を受け取る 3 つの場所
checkStock は3 点を超えると throw① checkStock(5)を直接呼ぶ② reserveStock(5).catch(...)③ awaitreserveStock(5)呼んだ行で例外が起きる返した Promise がrejected になるawait の行で例外が起きるtry に続くcatch 文.catch に渡した関数の引数try に続くcatch 文
② は呼んだ行で例外が起きず、Promise が rejected になります。catch 文に届くのは、その行で例外が起きる ① と ③ です

.catch と catch 文のどちらで受け取っても、届くのは同じ Error なので、error.messageinstanceof の判定はそのまま使えます。下の表は、この講座で扱った例外の書き方をまとめたものです。

書くもの役割注意
try / catch囲んだ行の例外を受け取る空の catch は失敗を消す
finally成否に関係なく最後に動く後片付けをまとめる
throw new Errorその場で例外を起こすnew Error だけでは止まらない
class extends Errorinstanceof で見分ける種類作った種類から先に判定
Error の cause元の例外を添えるerror.cause で読む
Promise の .catchrejected のときに呼ばれるthen の末尾に付ける
QUIZ

理解度チェック

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

Q1invoices.filter((invoice) => invoice.id === "B-302") が返すのはどれですか?

Q2invoices.filter(...).map(...).reduce(...) で、reduce が畳み込む配列は?

Q3コールバック方式で非同期の手順を 3 つ続けるとき、3 つ目の手順はどこに書きますか?