データ取得で try...catch しない理由
「データ取得で try...catch」とは、以下のようなものを指します。
try {
  const data = await fetchSomething();
  // 正常系レスポンスの処理
} catch (err) {
  if (isAxiosError(err)) {
    // 異常系レスポンスの処理
  }
}
動機はつぎの 3 つです。
- データ取得も宣言的に書きたいから
- データ取得に関係ない例外も catch してしまうから
- HttpError の集計に不便だから
データ取得も宣言的に書きたいから
要約すると、データ取得時は常にこのように書きたい、という話です。useSWR・useQuery や apollo/client でお馴染みのインターフェイスです。
const { data, err, status } = await fetchSomething();
if (data) // 正常系レスポンスの処理
if (err) // 異常系レスポンスの処理
レスポンスを型定義で表すと、このようなものになります。
type HttpResponse<T, K> = {
  data?: T;
  err?: K;
  status: number;
};
データ取得に関係ない例外も catch してしまうから
eslint の no-unused-expressions で以下のミスはすぐに気付きますが、any に握りつぶされている中間コードがもしあれば、catch 行きになる可能性が拭いきれません。
try {
  const data = await fetchSomething();
  // 参照ミスなどで例外がthrowされる
  a.b.c;
} catch (err) {
  if (isFetchError(err)) {
    // 異常系レスポンスの処理
  }
  // ReferenceError でここに着地する
}
こういった、データ取得とは関係ない例外と同列で catch 句ハンドリングしてしまうと、catch 句の肥大化・エラーの握りつぶしにつながりかねません。HttpError は開発者にとって想定範囲内なので、データ取得関数内部で整形し、畳み込んでおきたいです。
HttpError の集計に不便だから
理由としてはこれが一番大きいです。データ取得関数が全てHttpResponseのようなレスポンスを返すことが確約されていれば、
type HttpResponse<T, K> = {
  data?: T;
  err?: K;
  status: number;
};
Promise.all 後の取り回しが格段に楽になります。
const res: HttpResponse<T, K>[] = await Promise.all(
  fetchDataA(),
  fetchDataB(),
  fetchDataC(),
  fetchDataD()
);
- 全てのデータ取得が成功している場合、どうするか
- 一部のデータ取得が失敗している場合、どうするか
- status コードやエラーを集計し、二次処理をどうするか
List 型相当のレスポンスになるため、配列関数であれこれ処理ができます。複数データ並列取得が多い BFF の API Aggregation には特に便利です。
実装方法と CodeSandbox のサンプル
このレスポンスを得るために、何かライブラリを追加する必要はありません。普段使っている REST API クライアントに少し処理を挟めば十分です。例えば、Native fetch を利用している場合、then にバインドする関数で処理します。res.okで判断し、HttpData(正常系)とHttpError(異常系)に畳み込みます。
const url = `https://hacker-news.firebaseio.com/v0/newstories.json`;
// Before
fetch(url).then((res) => res.json());
// After
fetch(url).then((res) => {
  const { status, ok } = res;
  return res.json().then((d) => {
    if (ok) {
      // HttpData型(正常系)に畳み込む
      const res: HttpData = { data: d, status };
      return res;
    }
    // HttpError型(異常系)に畳み込む
    const res: HttpError = { err: d, status };
    return res;
  });
});
追記
Native fetch ではネットワークエラー時に reject されるため、結局ハンドリングのために try...catch で囲む必要があります(タイトルをひっくり返してしまいすみません)。また、200 番台以外を reject 扱いするのを再考してはどうか?という趣旨で書いた記事になりますが、Promise.all のユースケースによっては 400,500 番台を reject した方が都合が良いこともありますのでご了承ください。

Discussion