はじめに
エラーハンドリングは基本的にtry-catchで行っていますが、バグ等が原因で想定していない箇所でエラーが発生してしまうことがあるかもしれません。
自分以外の人に使ってもらうことを想定して個人開発したので、エラーがどこかで発生したら私は把握する必要があるため、エラーが出たらSlackに通知する等、何かした方がいいとスクールでアドバイスいただき、調べながら実装しました!
そこでNext.jsが用意している便利なerror処理の機能があることを知ったので、私が実装した手段についてまとめたいと思います!
師匠にはSlackに通知するコードを共有していただきましたが、どこでどんな風にその処理を呼び出すかは自分で考えました。
なので、「まさかすべての"use Client"ファイルに記述していくの・・?」とか考えてゾッとしたのですが調べたら全然違いました
前提
Next.jsのApp Routerを使っています。
error.jsとglobal-error.jsを発見
error.js
最初にerror.jsなるものを見つけてなんかいい感じの見つけたぞと喜んでいました。
ただ公式によると、
ファイル規則により、ネストされたルートerror.jsで予期しないランタイム エラーを適切に処理できます。
「ネストされた」がちょっと引っ掛かりました。
ルートapp/error.js境界は、ルートまたはコンポーネントでスローされたエラーをキャッチしません。app/layout.jsapp/template.js
公式を読む限り多分、error.jsは本当にコンポーネントとして他のコンポーネントでimportとして使うイメージっぽいです。
私がしたいのは、「どこでもいいどこかで発生したエラーをキャッチしてslack通知したい。」だったので、なんか違うなぁとにかく全部拾ってほしいなぁ。一発で済みそうなのないんかなと思っていると見つけました。(正確には公式下に読み進めると普通に勧められましたw)
global-error.js
こちらは私が求めていたものそのものという感じでした。
global-error.jsは
エラー境界はアプリケーション全体をラップし、アクティブな場合はそのフォールバック コンポーネントがルート レイアウトを置き換えます。
とあります。
global-error.js最も粒度の細かいエラー UI であり、アプリケーション全体の「包括的な」エラー処理と考えることができます。
包括的なエラー処理!!これですね。
ということで私がglobal-error.jsを採用することに決めました。
重要なポイント
公式には「知っておくといいこと」と書かれていましたが。
global-error.jsは本番環境でのみ有効です。開発環境では、代わりにエラー オーバーレイが表示されます。
ローカル環境でテスト出来ないのは私的に大変だったポイントです。
毎回Vercelのプレビューデプロイで検証だったので。
最終的なコード
"use client";
import Link from "next/link";
import { useEffect, useCallback } from "react";
import { Button } from "./_components/Button";
import { useApi } from "./_hooks/useApi";
import { PostRequest } from "./_types/apiRequests/error/PostRequest";
import { PostResponse } from "./_types/apiRequests/error/PostResponse";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const fetcher = useApi();
const post = useCallback(async () => {
try {
await fetcher.post<PostRequest, PostResponse>("/api/error", {
message: error.message,
});
} catch (error) {
console.error(error);
}
}, [fetcher, error]);
useEffect(() => {
if (!error) return;
post();
}, [error, post]);
return (
<html>
<body className="h-screen flex flex-col justify-center items-center bg-blue-200 text-gray-800">
<h2 className="text-center">エラーが発生しました。</h2>
<div className="mt-5 h-10 w-32 flex justify-center">
<Button onClick={() => reset()} variant="contained-blu500">
再実行
</Button>
</div>
//省略
</body>
</html>
);
}
これをapp直下に置きました!!
global-error.jsはクライアントサイドで動くので、必ず"use client"になります
useEffect(() => {
if (!error) return;
post();
}, [error, post]);
useEffectでerrorを監視して、エラーに値があったらpost処理するという流れです。
errorに値ないとそもそもこの処理が走ることもないとは思うのですが念のため早期リターンの処理も入れました。
api/error/route.tsでslackに通知する処理しています。
エラーを発生させてみる
実装ある程度できて、postman使ってSlackに通知くることは検証できましたが、実際にエラーをキャッチしてくれるかどうかの検証にはなっていないですし、本場環境でないとという制約もあったので、新たにブランチ切ってエラーをスローする処理を加えました。
ただ、ここが結構私の時間を食った箇所で、適当な箇所でthrow new Error("test error")
しても、ビルドエラーが発生します。
本番環境でないと動かないので、ビルドエラー吐かない方法でエラーをスローする必要がありました。
AIに頼るとテスト環境でしか使わない環境変数を使った方法を勧められましたが、どれもビルドエラーでダメでした。
結局ログイン後の階層内のトップレベルのlayout.tsxの中でセッション情報がなければloginページに遷移させる処理をしていましたが、そこに処理を加えてエラーをスローしました。
useEffect(() => {
//エラー発生させる処理ここから
if (session?.user.id !== "") {
throw new Error("test error");
}
//ここまで
if (!isLoading && session == null) {
router.replace("/login");
return;
}
}, [isLoading, session, router]);
ログイン後なのでsession情報のuser.idには値が入っているはずなのでここの空判定なら行けるのではと思ったらいけました!!
この、エラーをどう発生させたらビルドエラー出さずにエラーを発生させられるかで半日くらいかかった気がします。
これで無事にログインしたらエラーが発生してslackに通知飛びました!
まとめ
erroe.jsは特定の範囲でエラー処理するためのもの
global-error.jsはアプリケーション全体のエラー処理をするもの
といった感じで、私の今回のケースのような、どこでもいいからtry-catchでハンドリングしなかった予期していないエラーが発生していたら通知が欲しいケースはglobal-error.jsを使うで良いのではないかと思います!