こんにちは、株式会社ファストコーディングのフルスタックエンジニア、独身貴族Fireです。
先日のツーリングで山道を走っていたとき、ナビアプリが圏外で固まったんです。地図は表示されない、リルートもされない、画面には何のメッセージもない。結局、勘で走って帰ってきました。
エラーが起きたときに何も伝えないのは、最悪のUXです。Webアプリも同じ。
AI駆動開発でNext.js App Routerのエラーハンドリング戦略を設計してみました。AIに「error.tsxとglobal-error.tsxの使い分け」を提案させたところ、仕組みは正しく理解していましたが、「ユーザーに何を伝えるべきか」のUX判断が甘かった。技術的に正しいエラーハンドリングと、実際のプロダクトで使えるエラーハンドリングには差があります。
Next.js App Routerのエラーハンドリングの基本
Next.js App Routerには、エラーを処理するための専用ファイルが3つあります。
| ファイル | 役割 | キャッチするエラー |
|---|---|---|
error.tsx | セグメント単位のエラーUI | そのルートセグメント内で発生したランタイムエラー |
global-error.tsx | アプリ全体のエラーUI | ルートレイアウト内のエラー(error.tsxでは捕捉できないもの) |
not-found.tsx | 404エラーUI | notFound()が呼ばれた場合、またはURLに対応するルートがない場合 |
重要なのは、error.tsxはClient Componentである必要がある点です。'use client'を付けなければなりません。Server Componentでレンダリング中に発生したエラーも、最も近いerror.tsxがキャッチします。
AIに設計を依頼する
AIにエラーハンドリング戦略の設計を依頼しました。ダッシュボードと設定画面を持つアプリを想定し、ユーザーにわかりやすいメッセージ、開発環境でのデバッグ情報、リトライ機能を要件として渡しました。
AIの提案と修正
error.tsx:セグメント単位のエラー
AIの初版には3つの問題がありました。英語のまま、ユーザーに何が起きたか伝えていない、開発環境でのデバッグ情報がない。修正版はこうなります。
// app/dashboard/error.tsx — 修正版
'use client';
import { useEffect } from 'react';
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error('Dashboard error:', error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center min-h-[400px] p-8">
<div className="text-center max-w-md">
<h2 className="text-xl font-bold mb-4">
データの読み込みに失敗しました
</h2>
<p className="text-gray-600 mb-6">
一時的な問題が発生しています。しばらく待ってからもう一度お試しください。
</p>
<button
onClick={() => reset()}
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
再読み込み
</button>
{process.env.NODE_ENV === 'development' && (
<details className="mt-6 text-left text-sm">
<summary className="cursor-pointer text-gray-500">
エラー詳細(開発環境のみ)
</summary>
<pre className="mt-2 p-4 bg-gray-100 rounded overflow-x-auto text-xs">
{error.message}
{error.stack && `\n\n${error.stack}`}
</pre>
</details>
)}
</div>
</div>
);
}修正のポイントは以下の通りです。
- ユーザーに伝わるメッセージ。「データの読み込みに失敗しました」は、何が起きたかが具体的にわかる
- 次のアクションを示す。「しばらく待ってからもう一度お試しください」と「再読み込み」ボタンで、ユーザーが何をすべきかが明確
- 開発環境でのデバッグ情報。
process.env.NODE_ENV === 'development'で出し分ける。この値はビルド時にインライン展開されるため、本番ビルドではこのブロック全体がバンドルから除去される
global-error.tsx:アプリ全体のエラー
ルートレイアウト自体でエラーが発生した場合、error.tsxではキャッチできません。global-error.tsxが必要です。
// app/global-error.tsx
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="ja">
<body>
<div className="flex flex-col items-center justify-center min-h-screen p-8">
<div className="text-center max-w-md">
<h2 className="text-xl font-bold mb-4">
予期しないエラーが発生しました
</h2>
<p className="text-gray-600 mb-6">
申し訳ございません。ページを再読み込みしてください。
問題が解決しない場合は、しばらくしてから再度お試しください。
</p>
<button
onClick={() => reset()}
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
再読み込み
</button>
</div>
</div>
</body>
</html>
);
}global-error.tsxには<html>と<body>タグを含める必要があります。ルートレイアウトが壊れている状態で表示されるため、レイアウトの外側で独立して動く必要があるからです。AIはこの点を正しく理解して提案してきましたが、lang="ja"を忘れていました。
not-found.tsx:404エラー
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] p-8">
<div className="text-center max-w-md">
<h2 className="text-xl font-bold mb-4">
ページが見つかりません
</h2>
<p className="text-gray-600 mb-6">
お探しのページは移動または削除された可能性があります。
</p>
<Link
href="/"
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 inline-block"
>
トップページに戻る
</Link>
</div>
</div>
);
}not-found.tsxはerror.tsxと異なり、Server Componentで動作します。'use client'は不要です。このファイルはnotFound()関数が呼ばれたとき、またはルートが存在しないときに表示されます。
Server Componentでのエラーハンドリング
Server Componentでデータを取得する際のパターンです。
// app/dashboard/page.tsx
import { notFound } from 'next/navigation';
async function getDashboardData() {
const res = await fetch('https://api.example.com/dashboard', {
next: { revalidate: 60 },
});
if (res.status === 404) {
notFound();
}
if (!res.ok) {
throw new Error(`API error: ${res.status}`);
}
return res.json();
}
export default async function DashboardPage() {
const data = await getDashboardData();
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">ダッシュボード</h1>
{/* データ表示 */}
</div>
);
}ポイントは2つです。
- 404はnotFound()で処理する。throwではなくnotFound()を使うと、not-found.tsxが表示される。APIが404を返した場合、ユーザーに見せるべきは「エラーが発生しました」ではなく「ページが見つかりません」
- それ以外のエラーはthrowする。throwされたエラーは最も近いerror.tsxがキャッチする。try/catchで飲み込まずに、Error Boundaryに任せる
AIが見落としていたポイント
エラーメッセージの出し分け
AIは全てのerror.tsxで同じメッセージを使っていました。しかし、画面によって伝えるべきメッセージは異なります。
| 画面 | エラーメッセージ | 理由 |
|---|---|---|
| ダッシュボード | 「データの読み込みに失敗しました」 | データ取得エラーが主因 |
| 設定画面 | 「設定の保存に失敗しました」 | 操作の失敗が主因 |
| フォーム送信 | 「送信に失敗しました。入力内容を確認してください」 | ユーザーのアクションに紐づく |
AIは技術的に正しいエラー処理は書けますが、画面の文脈に応じたメッセージの使い分けはプロダクトの理解がないとできません。
reset()の制約
AIの提案ではreset()を万能のリトライ手段として扱っていましたが、実際には制約があります。reset()はReactのError Boundaryのエラー状態をクリアし、子コンポーネントの再レンダリングを試みます。ただし、データの再取得は行いません。そのため、エラーの原因がAPIの不具合であれば、reset()しても同じエラーが再発します。
実務では、reset()に加えて「トップページに戻る」リンクも用意しておくのが安全です。リトライしてもダメだった場合にユーザーが行き止まりにならないようにするためです。
まとめ
今回は、AI駆動開発でNext.js App Routerのエラーハンドリング戦略を設計した過程を紹介しました。
エラーハンドリングでAIを活用する際のポイントは以下の3つです。
- error.tsx、global-error.tsx、not-found.tsxの3つを適切に配置する。セグメントごとにerror.tsxを配置し、ルートレイアウト用にglobal-error.tsxを用意する。not-found.tsxで404を処理する
- エラーメッセージは画面の文脈に合わせる。AIは汎用的な「Something went wrong」を出すが、実際のプロダクトでは「何が失敗したか」「何をすべきか」を具体的に伝える
- reset()だけに頼らない。reset()はデータの再取得を行わないため、API起因のエラーからは復旧できない。トップページへのリンクなど代替の導線を用意する
私たちの開発現場では、AIにerror.tsxのテンプレートを生成させてから、画面ごとのメッセージや導線を人間が調整するフローを採っています。技術的な骨格はAIが作り、UXの判断は人間が行う。エラーハンドリングはまさにこの役割分担が効く領域です。
株式会社ファストコーディングでは、AI駆動開発を取り入れたNext.jsの設計・実装をサポートしています。「エラー画面がデフォルトのまま」「ユーザーが困るエラー表示を改善したい」という方は、お問い合わせフォームからお気軽にご相談ください。

