データ取得で 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