🐜

React で発生するエラーハンドリングおよびユーザーへのヒントを、エラーの再現方法とともに考える

2024/11/12に公開

開発を行っていて一番苦手なのがエラーハンドリングです。なぜなら、エラーを再現し、この対応でエラーに対処できる!と自信を持つことが難しいからです。
今回は React のエラーハンドリングを、エラーの再現方法から考え、まとめていきます。

1. index.html でエラーを検知する

まずは index.html で検知できるエラーについてです。 React のアプリケーションに辿り着く前の段階で起きるもので、コードそのものの不具合というよりも、デプロイ時の不具合や端末及びブラウザの設定・バージョンなどに起因するものを拾い上げることができます。

1-1. そもそも HTML が読み込まれているかどうか

ページビューが白紙であるとき、その原因がなんであるかは非常に不透明です。簡単な文字列が表示されていることで、読み込みが成功しているかどうかを判別でき、その後の原因究明に一役買うことが期待されます。

<!DOCTYPE html>
<html>
  <head>
    ...
  </head>
  <body>
    <div id="root"></div>
    <div id="verification">
      <p>アプリケーションをロード中…</p>
    </div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

<div id="root"></div> の直後に検証用の HTML を置く div 要素を配置します。まずはここに恒久的な文章を入れておきましょう。そうすれば、ページが白紙になったとき、それがルーティング以前の問題であると考えることができます。

これより以下で解説する要素は、 id="verification" の子要素もしくは兄弟要素にあることを想定しています。

1-2. JavaScript が有効かどうか

React で開発されたアプリケーションは、JavaScript が無効になっていると正しく動作させられません。<noscript /> を用いて JavaScript が無効かどうかを検知し、これに当てはまる場合、JavaScript を有効にすることをユーザーに求めます。

<noscript>
  <p>
    ブラウザのJavaScript機能が無効になっています。<br />
    アプリケーションを利用するにはJavaScriptを有効化してください。
  </p>
</noscript>

https://reference.hyper-text.org/html/element/noscript/

1-3. ESModule に対応したブラウザかどうか

<div id="verification-unsupported-browser" hidden>
  最新のブラウザを利用してください。
</div>
<script nomodule>
  (function () {
    var element = document.getElementById("verification-unsupported-browser");
    if (element) {
      element.hidden = false;
    }
  })();
</script>

https://reference.hyper-text.org/html/attribute/nomodule/

2. React Router の ErrorElement で例外を処理する

2-0. 例外を受け取る準備

<Route path="/" element={<Home />} errorElement={<Error />} />

RouteerrorElement を渡すことで、例外が発生した際に表示されるページコンポーネントを指定することができます。
公式ドキュメントによると、バニラのエラー表現はエンドユーザーにとって見づらいものであるため、少なくともルートレベルにおいてなんらかのエラー表現を準備しておくことを推奨しているようです。

https://reactrouter.com/en/main/route/error-element

例外が発生すると、 useRouteError() を用いて中身を受け取ることができます。

import { useRouteError } from "react-router-dom";

const Error: FC = () => {
  const error = useRouteError();
  console.error(error);
};

1 章で明示する error は、全てこれを受け取っているものとします。

2-1. ブラウザが返す HTTP レスポンスステータスコードを用いる(ex. 404 ルーティングエラー)

import { isRouteErrorResponse } from "react-router-dom";

isRouteErrorResponse(error); // boolean

isRouteErrorResponse() を用いることで、例外が ErrorResponseImpl としてスローされたことを検証できます。
中身を確認すると分かる通り、 ErrorResponseImpl には ErrorResponse が含まれています。 ErrorResponse.status を用いることで、ブラウザが返すすべてのレスポンスコードに対応できると考えられます。

ここでは、一番代表的なエラーともいえる 404 について補足します。

2-1-1. 対応例)404 ルーティングエラー

const isPageNotFound = isRouteErrorResponse(error) && error.status === 404;

App.tsx でパスとして登録されていない文字列を用いたアクセスがあった場合に表示されます。このハンドリングが機能するとき、ユーザーが URL を誤っているか、ブックマーク等から削除済みのパスを用いて遷移していることが考えられます。アナウンス及び機能としては、URL を確認して再度アクセスしてもらうことや、元いたページに戻ることが挙げられるでしょう。

2-2. TypeError の中身を見てエラー内容を確認する

const isTypeError: boolean = error instanceof TypeError;

スローされた例外が TypeError であることを検証しています。例えば ErrorResponseImpl がスローされたとき、これは false になり、これ以降のコードを読まれることはありません。

2-2-1. 関数エラー

const isFunctionError = error instanceof TypeError && error.message.includes("is not a function");

ここでは、 error.messagenot a function が含まれることを確認しています。これにより、関数エラーが原因で例外がスローされている可能性が高くなります。

開発者としてこのエラーを起こすには、JavaScript の関数の末尾に一文字位追加するなどし、存在しない関数に変更する方法が考えられます。

このハンドリングが機能するのは、例えば以下のような場合です:

console.log(`
  - ローカル環境:${isFunctionError} // false
  - 本番環境 ブラウザ1:${isFunctionError} // false
  - 本番環境 ブラウザ2:${isFunctionError} // true
`);

このように、関数エラーが環境の違いで発生するとき、考えられる要因はブラウザのバージョンが古いことです。
例えば JavaScript で用いられるメソッドは、古い IE 等でサポートされていない場合があります。仮にこういったブラウザをサポート外とする場合、「ブラウザがサポート外であることが考えられる」ことと、ユーザーが取ることのできるアクションを明示すれば、行き止まりを防ぐことができるかもしれません。

2-2-2. API エラー

const isFetchError: boolean =
  error instanceof TypeError && error.message.includes("Failed to fetch");

はじめに 1-2 と同じく、スローされた例外が TypeError であることを検証しています。
次に、error.messageFailed to fetch が含まれることを確認しています。これにより、fetch エラーが原因で例外がスローされている可能性が高くなります。

開発者としてこのエラーを起こすには、API のエンドポイントを誤った文字列に変更する方法が考えられます。

このハンドリングが機能するとき、ユーザーがアクションを起こすことによってこれを抜け出すのは難しいでしょう。なぜなら API に起因するエラーならばローカル環境でも起こっている可能性が高く、その場合対策を打つべきは開発者だからです。
ユーザーへのヘルプに専門的な言葉を用いるのは不適切であることもあり、不明なエラーが発生したとしてリロードを促すことが、対応策としては限界なのではないかと思います。

3. これらをすり抜けた例外をキャッチする

3-1. navigator を用いてチェックする

3-1-1. 端末がオフライン

import { FC, ReactNode } from "react";
import Error from "~/pages/Error";

type Props = {
  children: ReactNode;
};

const NetworkCheckWrapper: FC<Props> = ({ children }) => {
  const isOffLine = !navigator.onLine;

  const [errorHeading, errorMessage] = isOffLine
    ? ["端末がオフラインです", ""]
    : ["", ""];

  if (!!errorHeading.length || !!errorMessage.length) {
    return <Error errorObject={{ errorHeading, errorMessage }} />;
  }

  return children;
};

export default NetworkCheckWrapper;

おわりに

エラーは際限がなくあるものですが、代表的なチェック法や対処法を覚えておくと、ある程度のカバーができると思います。今後も新しいことに触れた場合は追記していきます。

Discussion