はじめに — エラー画面がないことに気づいた日
「エラーが起きたとき、ユーザーには何が表示されるんだっけ?」
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 とこの方針を議論しました。
Zeronova
error.tsx でエラーメッセージをどこまで出すべきか悩んでいる。開発中は詳細なエラー情報が欲しいけど、本番でスタックトレースを見せるのはまずい。Claude Codeprocess.env.NODE_ENVで出し分けるのが定石です。開発環境ではスタックトレースを表示し、本番環境では汎用的なメッセージにします。加えて、error.digest プロパティのハッシュ値をサーバーログと照合できるようにしておくと、本番でのデバッグにも役立ちます。Zeronova
なるほど。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 の実装パターンに関連する記事:
- Next.js の "use server" 制約と回避策 — error.tsx が必要になった直接のきっかけ
- Next.js App Router で Supabase 認証を使う — middleware.ts がカギ — ルーティングとミドルウェアの全体設計
- Next.js App Router で SEO 最適化する完全ガイド — App Router の機能を活かした SEO 対応
まとめ
BandBridge の開発中に「エラー画面がない」問題に気づき、error.tsx と not-found.tsx を実装しました。
持ち帰りポイント:
- error.tsx は Client Component(
"use client"必須) — React Error Boundary として自動的に機能する - not-found.tsx は
notFound()関数と組み合わせて使う — 動的ルートでデータが見つからない場合に呼ぶ - 開発環境と本番環境でエラー表示を出し分ける — 開発中はスタックトレース、本番は汎用メッセージ
- 同階層の layout.tsx のエラーはキャッチできない — 親ディレクトリの error.tsx か global-error.tsx が必要
「エラー画面の整備」は後回しにされがちですが、ユーザー体験に直結する部分です。「動いている」ことに安心せず、「壊れたときにどう見えるか」を確認しておくことが大切です。BandBridge では Claude Code と一緒にエラーハンドリングを整備した結果、開発中のデバッグ効率も大幅に向上しました。
Zeronova(ゼロノバ)
Product Manager / AI-Native Builder
Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。