React で発生するエラーハンドリングおよびユーザーへのヒントを、エラーの再現方法とともに考える
開発を行っていて一番苦手なのがエラーハンドリングです。なぜなら、エラーを再現し、この対応でエラーに対処できる!と自信を持つことが難しいからです。
今回は 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>
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>
2. React Router の ErrorElement で例外を処理する
2-0. 例外を受け取る準備
<Route path="/" element={<Home />} errorElement={<Error />} />
Route
に errorElement
を渡すことで、例外が発生した際に表示されるページコンポーネントを指定することができます。
公式ドキュメントによると、バニラのエラー表現はエンドユーザーにとって見づらいものであるため、少なくともルートレベルにおいてなんらかのエラー表現を準備しておくことを推奨しているようです。
例外が発生すると、 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.message
に not 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.message
に Failed to fetch
が含まれることを確認しています。これにより、fetch エラーが原因で例外がスローされている可能性が高くなります。
開発者としてこのエラーを起こすには、API のエンドポイントを誤った文字列に変更する方法が考えられます。
このハンドリングが機能するとき、ユーザーがアクションを起こすことによってこれを抜け出すのは難しいでしょう。なぜなら API に起因するエラーならばローカル環境でも起こっている可能性が高く、その場合対策を打つべきは開発者だからです。
ユーザーへのヘルプに専門的な言葉を用いるのは不適切であることもあり、不明なエラーが発生したとしてリロードを促すことが、対応策としては限界なのではないかと思います。
3. これらをすり抜けた例外をキャッチする
navigator
を用いてチェックする
3-1. 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