Closed7

React + SWR、ケース別にエラーハンドリングしたい

OpionOpion

Webアプリケーションで発生しうるエラーをざっくり分けると

  • 予期されたエラー
    • ユーザー操作に対する同期的なエラー(基本的にdisabledで対処すべきとかはさておき)
    • 非同期処理によるエラー
    • ライブラリから投げられるエラー
  • 予期せぬエラー
    • 通信失敗、404アクセス、混雑などそもそも鯖に到達しないエラー
    • クライアントサイドでのクラッシュ

予期せぬエラーに対してはErrorBoundaryやカスタムエラーページで対応するとして、本題は予期されたエラーをどうハンドリングしていくか。

OpionOpion

かつては中央集権的に、たとえばAxiosInterceptorだったりthrow APP_ERROR['Foo'] といった手法で巨大なErrorHandlerクラスやhooksに対応を投げていた。

しかしこのやり方には以下のような問題があった。

  • throwされたエラーを受け取る箇所では非同期も同期もまとめて処理していた
  • 異なるAPI/同じAPIだが異なるケースで同じエラーが吐かれたとき区別することが困難で、別途catchが必要になりチグハグであった
  • ページやhooksは正常系にのみ関心を持っていたが、異常系の処理が重要なケースであっても不意に分離されてしまっていた

そのため、予期可能な異常系が存在する操作で

// 同期
const handleClick = (event) => {
  if (event.target....) {
    handleFooError(event);
    return;
  };
  // 以降正常処理
};

// 非同期
const {data, error} = useSWR(...);

// ライブラリ
player.on('error', (error) => {
  handlePlayerError(error);
});

といった方針を考えてみる。

OpionOpion

このような方針にすることによって

同期エラー

メリットが大きい。UIに影響するケースが多いのでエラー発生状況とリアクションが独特になりやすい。

非同期エラー

実態はほぼ変わらないが「中央集権的だが例外はある」から「個別に処理するが概ね共通のものを呼び出す」になる。SWRの {data, error} で無理なく実装できそうだが

const {data, error} = useSWR(path, fetcher, { onError: handleSwrError });

// 原則ここにuseEffectを使わないけど
useEffect(() => error && handleSwrError(error), []);

はuseSWRのonErrorが優先して両方発火する。
ここら辺どういった作法で書いていくべきかはあとで考える。

ライブラリエラー

もっとも影響が少ないかも。
大抵throwしてくれてるのを改めてライブラリ使用側で拾うが、共通のErrorHandlerで拾うか使用しているまさにその付近で拾うかの違いでしかない。この違いが大きいケースもあるかもしれないが、その場合はもともとライブラリに搭載されているエラー処理に従うケースが多かった(?)

OpionOpion

以降は非同期エラーのハンドリング、特にSWR利用時について考える。
SWRに備わっているエラーハンドリングの手法は以下の3つ。

// 1. SWRConfig
<SWRConfig onError={handleError}>{children}</SWRConfig>

// 2. useSWR onError
const { data } = useSWR(path, fetcher, { onError: handleError })

// 3. useSWR returned error
const { data, error } = useSWR(path, fetcher);

1は今回の主旨から外れる(共通エラーがあるならhandleErrorといった処理関数側に閉ざす)ため2, 3を考慮したい。おそらく

// A. 引数でハンドラーを呼ぶ
const { data } = useFetchHistory(handleFetchHistoryError);

// B. hooksに閉ざす
const { data } = useFetchHistory(); // 中でonErrorを定義してる

// C. errorを出してhooksを読んだ側で処理させる
const { data: history, error } = useFetchHistory();
useHandleHistoryError(error);

return (
  <section>
    <h2>なんとか履歴</h2>
    <Suspense fallback={<SkeltonList />}>
      <HistoryList {...history} />
    </Suspense>
  </section>
)
OpionOpion

「同じAPI呼んで同じエラー出たけど、呼んだケースによってエラーを分けたい等」はhooks自体を分けてしまえばいい。
非同期のエラー処理は非同期処理の1つであるからuseSWRを呼ぶhooksの責務(?)

useFetchHistory()
useFetchLimitedHistory()

==================

export function useFetchHistory() {
  const { handleApiError } = useApiError();
  const handleError = (error) => {
    if (error...) { /** 専用の処理 */ return; };
    handleApiError(error);
  }
  const { data } = useSWR(path, fetcher, { onError: handleError })
  return { data }
}
OpionOpion

ひとまずこの方針でやってみて、今までの辛さが解消されるか、別の辛さが生まれるかなど追って記述する。

OpionOpion

稀にあるuseSWRのmutateを使いたくいケースで個別の対応が必要になる……けどそれくらいかも。
おおむね満足してる

このスクラップは2023/08/04にクローズされました