データ取得で try...catch しない理由

12301 ワード

「データ取得で 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 した方が都合が良いこともありますのでご了承ください。