Next.js App Router の error.tsx と not-found.tsx でエラーハンドリングを実装する

2026.02.10
Share:

はじめに — エラー画面がないことに気づいた日

「エラーが起きたとき、ユーザーには何が表示されるんだっけ?」

BandBridge(ミュージシャンマッチングサービス)の開発中、ふとこの疑問が浮かびました。答えは「何も表示されない」でした。正確には、Next.js のデフォルトのエラー画面 — 白い背景に "Application error: a client-side exception has occurred" と表示される、あのそっけない画面です。

2026年1月10日の開発日記にはこう書いています:

error.tsx / not-found.tsx: エラー時のUIがなかったため、ユーザーに何が起きたか伝えられなかった

開発中はコンソールでエラーを確認するため、エラー画面の不在に気づきにくいものです。しかし、本番環境でユーザーが遭遇するのは画面上の表示だけです。

背景 — "use server" 制約がきっかけ

エラーハンドリングの実装に着手したきっかけは、別の問題の修正中でした。BandBridge のメッセージチャット画面で "use server" ファイルから定数をエクスポートしようとしてエラーが発生していました。

開発日記にはこう記録しています:

report-types.ts作成("use server"ファイルからの定数エクスポート問題)

Server Actions のファイル("use server" ディレクティブ付き)からは、関数以外のもの — 定数や型 — をエクスポートできないという制約があります。この制約に違反すると実行時エラーになりますが、エラー画面がないためユーザーには白い画面だけが表示されていました。

"use server"ディレクティブのあるファイルから定数をエクスポートするとエラーになる。型/定数を別ファイルに分離

この問題自体は型定義ファイルを分離することで解決しましたが、「そもそもエラーが起きた時のUIがない」という根本的な問題に気づかされました。Claude Code にエラーハンドリングの実装を依頼し、error.tsx と not-found.tsx を追加することにしました。

error.tsx — React Error Boundary として機能する

Next.js App Router の error.tsx は、自動的に React Error Boundary として機能します。同じディレクトリ(またはその子ディレクトリ)内のコンポーネントで発生したエラーをキャッチし、フォールバックUIを表示します。

基本的な実装

"use client"; // error.tsx は必ず Client Component

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="flex flex-col items-center justify-center min-h-[50vh]">
      <h2 className="text-xl font-bold mb-4">
        エラーが発生しました
      </h2>
      <p className="text-gray-500 mb-6">
        申し訳ありません。問題が発生しました。
      </p>
      <button
        onClick={() => reset()}
        className="px-4 py-2 bg-primary text-white rounded"
      >
        もう一度試す
      </button>
    </div>
  );
}

ポイント

"use client" が必須: error.tsx は React の Error Boundary を利用するため、必ず Client Component である必要があります。"use client" を忘れるとビルドエラーになります。

reset 関数: ユーザーがエラーから復帰できるよう、reset() を呼ぶとコンポーネントツリーの再レンダリングが試みられます。一時的なネットワークエラーなどは、これだけで解決することがあります。

error.digest: 本番環境では、セキュリティのためにエラーの詳細が隠されます。代わりに digest プロパティにハッシュ値が入り、サーバーログと照合できます。

開発環境と本番環境での出し分け

error.tsx を実装する際、「どこまでの情報を表示するか」が設計上の判断ポイントでした。Claude Code とこの方針を議論しました。

ZeronovaZeronova
error.tsx でエラーメッセージをどこまで出すべきか悩んでいる。開発中は詳細なエラー情報が欲しいけど、本番でスタックトレースを見せるのはまずい。
Claude Code
process.env.NODE_ENV で出し分けるのが定石です。開発環境ではスタックトレースを表示し、本番環境では汎用的なメッセージにします。加えて、error.digest プロパティのハッシュ値をサーバーログと照合できるようにしておくと、本番でのデバッグにも役立ちます。
ZeronovaZeronova
なるほど。error.tsx は Client Component だから useEffect でエラー監視サービスへの送信もできるか。まずはシンプルに出し分けから始めよう。

開発日記にもこう書いています:

エラーバウンダリの設計: どこまでの情報を表示するか。開発環境ではスタックトレース、本番では汎用メッセージ

この方針を実装に反映すると:

"use client";

export default function Error({ error, reset }: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="flex flex-col items-center justify-center min-h-[50vh]">
      <h2 className="text-xl font-bold mb-4">
        エラーが発生しました
      </h2>

      {process.env.NODE_ENV === "development" ? (
        <pre className="text-sm text-red-400 bg-gray-900 p-4 rounded mb-4 max-w-lg overflow-auto">
          {error.message}
          {"\n"}
          {error.stack}
        </pre>
      ) : (
        <p className="text-gray-500 mb-4">
          申し訳ありません。しばらくしてからもう一度お試しください。
        </p>
      )}

      <button onClick={() => reset()}>もう一度試す</button>
    </div>
  );
}

開発環境ではエラーの原因をすぐに特定するためにスタックトレースを表示し、本番環境ではユーザーに不要な技術情報を見せないようにします。

not-found.tsx — 存在しないページの処理

not-found.tsx は、存在しないルートにアクセスした場合のフォールバックUIです。Next.js の notFound() 関数と組み合わせて使います。

基本的な実装

import Link from "next/link";

export default function NotFound() {
  return (
    <div className="flex flex-col items-center justify-center min-h-[50vh]">
      <h2 className="text-2xl font-bold mb-4">
        ページが見つかりません
      </h2>
      <p className="text-gray-500 mb-6">
        お探しのページは存在しないか、移動した可能性があります。
      </p>
      <Link
        href="/"
        className="px-4 py-2 bg-primary text-white rounded"
      >
        トップページに戻る
      </Link>
    </div>
  );
}

notFound() 関数との連携

動的ルート([slug] など)で、該当するデータが見つからない場合に notFound() を呼びます:

import { notFound } from "next/navigation";

export default async function ProfilePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const user = await getUser(id);

  if (!user) {
    notFound(); // not-found.tsx が表示される
  }

  return <UserProfile user={user} />;
}

notFound() は例外をスローするため、呼び出し後のコードは実行されません。return notFound() と書く必要はありません(書いても動きますが冗長です)。

"use server" 制約との関連

BandBridge でこの問題に遭遇した直接のきっかけは、"use server" ファイルからの定数エクスポートでした。この制約については Next.js の "use server" 制約と回避策で詳しく書きましたが、要点をここでも触れておきます。

// ❌ NG: "use server" ファイルから定数をエクスポート
"use server";

export const REPORT_TYPES = ["bug", "feature", "other"] as const;

export async function submitReport(type: string) {
  // ...
}
// ✅ OK: 定数・型は別ファイルに分離
// report-types.ts("use server" なし)
export const REPORT_TYPES = ["bug", "feature", "other"] as const;
export type ReportType = (typeof REPORT_TYPES)[number];

// report-actions.ts
"use server";
import { REPORT_TYPES } from "./report-types";

export async function submitReport(type: string) {
  // ...
}

開発日記にも記録しています:

"use server"ファイルは関数のみエクスポート可能。定数・型は別ファイルに

この制約に違反するとランタイムエラーになります。error.tsx がなければ、ユーザーにはただの白い画面が表示されるだけです。

ディレクトリ構造とスコープ

error.tsx と not-found.tsx は、配置するディレクトリによってスコープが変わります。

app/
├── error.tsx           # アプリ全体のエラーフォールバック
├── not-found.tsx       # アプリ全体の404フォールバック
├── dashboard/
│   ├── error.tsx       # /dashboard 以下のエラーフォールバック
│   └── page.tsx
└── profile/
    └── [id]/
        ├── error.tsx   # /profile/[id] のエラーフォールバック
        └── page.tsx

ディレクトリが深い方の error.tsx が優先されます。/dashboard でエラーが起きた場合、app/dashboard/error.tsx があればそちらが使われ、なければ app/error.tsx にフォールバックします。

注意点: layout.tsx のエラーはキャッチできない

error.tsx は同じ階層の layout.tsx で発生したエラーをキャッチできません。layout.tsx のエラーをキャッチするには、親ディレクトリの error.tsx が必要です。

app/
├── error.tsx           # ← app/layout.tsx のエラーはキャッチできない
├── layout.tsx
└── (routes)/
    ├── error.tsx       # ← app/(routes)/layout.tsx のエラーをキャッチ
    └── layout.tsx

ルートの app/layout.tsx でエラーが発生した場合、app/error.tsx ではキャッチできません。これは Next.js App Router の仕様です。ルートレイアウトのエラーには app/global-error.tsx を使います。

実践的な設計パターン

パターン1: アプリ全体のフォールバック + セクション別カスタマイズ

app/
├── error.tsx           # 汎用メッセージ
├── not-found.tsx       # 汎用404
├── dashboard/
│   └── error.tsx       # 「データの読み込みに失敗しました」+ リロードボタン
└── auth/
    └── error.tsx       # 「認証エラーが発生しました」+ ログインリンク

セクションごとにメッセージを変えることで、ユーザーが次に何をすべきか明確になります。

パターン2: エラーロギングの統合

"use client";

import { useEffect } from "react";

export default function Error({ error, reset }: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // エラー監視サービスに送信
    console.error("Unhandled error:", error);
  }, [error]);

  return (/* UI */);
}

error.tsx は Client Component なので、useEffect でエラーログをサービスに送信できます。Sentry や LogRocket との統合もここで行えます。

関連記事

Next.js App Router の実装パターンに関連する記事:

まとめ

BandBridge の開発中に「エラー画面がない」問題に気づき、error.tsx と not-found.tsx を実装しました。

持ち帰りポイント:

  1. error.tsx は Client Component("use client" 必須) — React Error Boundary として自動的に機能する
  2. not-found.tsx は notFound() 関数と組み合わせて使う — 動的ルートでデータが見つからない場合に呼ぶ
  3. 開発環境と本番環境でエラー表示を出し分ける — 開発中はスタックトレース、本番は汎用メッセージ
  4. 同階層の layout.tsx のエラーはキャッチできない — 親ディレクトリの error.tsx か global-error.tsx が必要

「エラー画面の整備」は後回しにされがちですが、ユーザー体験に直結する部分です。「動いている」ことに安心せず、「壊れたときにどう見えるか」を確認しておくことが大切です。BandBridge では Claude Code と一緒にエラーハンドリングを整備した結果、開発中のデバッグ効率も大幅に向上しました。

Zeronova avatar

Zeronovaゼロノバ

Product Manager / AI-Native Builder

Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。

関連プロダクト

BandBridge(バンドブリッジ)

ミュージシャンとバンドをつなぐ