例えば以下の関数があるとします。これは 1秒後にリゾルブまたはリジェクトされるプロミスを返す関数です。どちらになるかは 2 分の 1 でランダムに決まります。

const mayReject = () => {
  return new Promise((resolve, reject) => {
    const shouldReject = Math.random() >= 0.5;
    const settle = shouldReject ? reject : resolve;
    const message = shouldReject ? 'rejected' : 'resolved';
    setTimeout(() => settle(message), 1000);
  });
};

async await のエラーハンドリングは try catch で行うのが一般的なので、例えばこの関数を呼び出すときは以下のように書きます。

3回呼び出している mayReject() のどれかがリジェクトされた場合はその時点で catch に流れます。

try catch 方式

(async () => {
  try {
    await mayReject();
    await mayReject();
    await mayReject();
  } catch (e) {
    console.error(e);
  }
})();

上記で何ら問題はありませんが、以下のように書くこともできます。動きは全く同じです。

catch 方式

(async () => {
  await mayReject();
  await mayReject();
  await mayReject();
})().catch(console.error);

try catch のブロックがなくなったことにより少しスッキリしました。

1つずつエラーハンドリングするなら catch 方式が断然綺麗

それぞれの mayReject() を個別にエラーハンドリングする場面を想定します。

try catch では以下の記載になります。

try catch 方式

(async () => {
  try {
    await mayReject();
  } catch (e) {
    console.error(e);
  }
  try {
    await mayReject();
  } catch (e) {
    console.error(e);
  }
  try {
    await mayReject();
  } catch (e) {
    console.error(e);
  }
})();

重たい記述になりました。

一方で単純な catch を利用すると以下のように書くことができます。

catch 方式

(async () => {
  await mayReject().catch(console.error);
  await mayReject().catch(console.error);
  await mayReject().catch(console.error);
})();

スッキリしています。

catch 方式の別の利点

単純な catch の方では別の利点もあります。

try catch の場合、try ブロック内で非同期関数から受け取った結果をブロック外で使う場合は以下のようにする必要があります。

try catch 方式

(async () => {
  let res1;
  let res2;
  let res3;

  try {
    res1 = await mayReject();
    res2 = await mayReject();
    res3 = await mayReject();
  } catch (e) {
    console.error(e);
  }

  console.info(res1);
  console.info(res2);
  console.info(res3);
})();

このように、なかなか受け入れ難い書き方にならざるを得ません。

その点、単純な catch であれば try ブロックによりスコープを作られることもありませんので、通常通り書くことができます。

catch 方式

(async () => {
  const res1 = await mayReject();
  const res2 = await mayReject();
  const res3 = await mayReject();
  console.info(res1);
  console.info(res2);
  console.info(res3);
})().catch(console.error);

補足:エラーハンドリングの過程が若干異なる

try catch 方式と catch 方式は同じ動きをするものの、そこまでの過程が若干異なります。

JavaScript の仕様として、await で待っている Promise がリゾルブされるとその結果を返しますが、リジェクトされた場合はエラーをスローします。

そのため、try catch 方式がエラーハンドリングする流れは以下です。

try catch 方式

(async () => {
  try {
    await mayReject(); // 1. リジェクトされたらエラーをスロー
  } catch (e) {
    console.error(e); // 2. スローされたエラーを catch する
  }
})();

このとき await で呼び出されている関数が try catch で囲まれていない場合、その親の関数はリジェクトを返します。

ここでいう親の関数とは IIFE (async () => {})(); のことです。

つまり、catch 方式がエラーハンドリングする流れは以下です。

catch 方式

// 2. 内部でエラーがスローされたのでリジェクトを返す
(async () => {
  await mayReject(); // 1. リジェクトされたらエラーをスロー
})().catch(console.error); // 3. リジェクトされたことを catch する

おまけ:catch を複数用いた場合

最後におまけです。以下の mayReject() がリジェクトされるとコンソールには何が出力されるでしょうか。

(async () => {
  try {
    await mayReject().catch(console.error('catched 1'));
  } catch {
    console.error('catched 2');
  }
})();
(async () => {
  await mayReject().catch(console.error('catched 1'));
})().catch(() => console.error('catched 2'));

結果はどちらも以下。両方出力されます。

catched 1
catched 2

ではこれはどうでしょう?

(async () => {
  try {
    await mayReject().catch(console.error('catched 1'));
  } catch {
    console.error('catched 2');
  }
})().catch(() => console.error('catched 3'));

こちらも結果は同様です。catched 3 は出力されません。

catched 1
catched 2