Next.js appRouter エラーハンドリングについて

Next.js

公開日時:2024/09/15

error.js(公式)

appRouterにおいて、error.jsを作成することでツリー内でthrowされたErrorを拾ってエラーページをハンドリングできる。
しかしdev環境とbuild環境では挙動(Errorの扱い)が異なる。

結論だけ言うと、build環境では error message が上書きされてしまうのだ。

error.jsを作成する

app/error.tsxを作成する。単純にerror.messageとerror.digestを表示するだけのコンポーネントにする。
digestはappRouterでthrowされるErrorが自動的に拡張しているものになる。

app/error.tsx
'use client';

export default function GlobalError({
  error,
}: {
  error: Error & { digest: string };
}) {
  return (
    <html lang='ja'>
      <body>
        <h1>Error</h1>
        <pre>{error.message}</pre>
        <pre>{error.digest}</pre>
      </body>
    </html>
  );
}

CustomErrorを作成する

通常のErrorは throw new Error('error') のように messageを引数にとる。
先述したようにdigestを拡張したErrorを CustomErrorとして定義する。この理由については後述する。

app/_customError.ts
export class CustomError extends Error {
  digest: string;

  constructor(message: string, digest?: string) {
    super(message);
    this.digest = digest || 'digest';
  }
}

dynamic routeでCustomErrorをthrowするpageを作成する

例えば、dynamic routeでpageの[id]が4桁ではない場合に400エラーにするようなエラーハンドリングを想定する。
この時に取れる方法は2つ。

  1. page.tsx内で400エラー用のコンポーネントをreturnする。
  2. error.jsでハンドリングする。(注意)

2についての挙動がこの記事のメイン。

まずは分岐を実装する。
messageは custom Error
digestは code=400
とする。

app/[id]/page.tsx
import { CustomError } from '../_customError';

export default async function Page({ params }: { params: { id: string } }) {
  if (!params.id.match(/\d{4}/)) {
    throw new CustomError('custom error', 'code=400');
  }
  return (
    <div>
      <h2>{params.id} page</h2>
    </div>
  );
}

この時にErrorをthrowした場合の挙動が dev環境と build環境で異なる。

dev環境

messageは custom Error
digestは code=400
実装通りのものがthrowされていることがわかる

build環境

digestは code=400 で変わらないが
messageは上書きされてNext.jsが設定したエラーメッセージが表示されてしまっている。

もし、次のように error.js内で error.message で分岐を実装していた場合、dev環境とbuild環境で結果が変わってしまう。

app/error.tsx
'use client';

export default function GlobalError({
  error,
}: {
  error: Error & { digest: string };
}) {
  if (error.message === 'custom error') {
    return (
      <html lang='ja'>
        <body>
          <h1>Custom Error</h1>
          <pre>{error.message}</pre>
          <pre>{error.digest}</pre>
        </body>
      </html>
    );
  }

  return (
    <html lang='ja'>
      <body>
        <h1>Error</h1>
        <pre>{error.message}</pre>
        <pre>{error.digest}</pre>
      </body>
    </html>
  );

その対策で、error.digestで分岐をするのが良さそう。

error.js error.digestで分岐を実装したサンプル

app/error.tsx
'use client';

export default function GlobalError({
  error,
}: {
  error: Error & { digest: string };
}) {
  if (error.digest === 'code=400') {
    return (
      <html lang='ja'>
        <body>
          <h1>Bad Request</h1>
        </body>
      </html>
    );
  }

  return (
    <html lang='ja'>
      <body>
        <h1>Error</h1>
        <pre>{error.message}</pre>
        <pre>{error.digest}</pre>
      </body>
    </html>
  );
}