Next.js Error Boundary 完全ガイド:ランタイムエラーを優雅に処理する5つの重要なテクニック

午前3時、携帯が振動しました。目を開けて見ると、運営チームのグループチャットが爆発していました。「トップページが開かない! 真っ白だ!」
監視プラットフォームを開くと、血の気が引きました。あるサードパーティ製コンポーネントがダウンし、ページ全体を道連れにしていたのです。ユーザーが見ていたのは、エラーメッセージさえない、ただの惨めな白い画面でした。
正直なところ、こういうことは一度や二度ではありません。あなたも似たような経験があるかもしれません。本番環境で順調に動いていたページが、データのフォーマット違反やAPIのタイムアウトが原因で突然クラッシュする。従来の try-catch では React コンポーネントのレンダリング層をカバーできず、その結果、ユーザーは白い画面を見つめて呆然とし、そっとページを閉じることになります。
ユーザー体験の調査によると、ホワイトスクリーン(真っ白な画面)は80%以上のユーザーを即座に離脱させます。これは恐ろしい数字です。
幸い、Next.js には Error Boundary という仕組みがあり、これらのランタイムエラーを優雅に処理できます。ホワイトスクリーンを回避するだけでなく、ユーザーにフレンドリーなフォールバック画面を提供したり、「再試行」ボタンで復旧させたりすることも可能です。この記事では、基本的な error.tsx から、全体をカバーする global-error.tsx、そして Server Components での特別な処理まで、Next.js Error Boundary の完全な使い方を解説します。
これを読めば、エラー発生時にもアプリを優雅に振る舞わせ、夜中にバグ修正で叩き起こされる悲劇から解放されるはずです。
なぜ Error Boundary が必要なのか? 従来のエラー処理の限界
React を使い始めた頃、すべては try-catch で解決できると思っていました。しかし、すぐに現実に直面しました。
try-catch の3つの致命的な弱点
まず1つ目:これらは同期コードのエラーしか捕捉できません。try ブロック内で JSON.parse(badData) を書けば捕捉できますが、コンポーネントのレンダリング中にエラーが起きた場合は? 残念ながら、捕捉できません。
2つ目はもっと厄介です:イベントハンドラ内の非同期エラー。例えば、クリックイベントで API を呼び出し、その API がダウンしていた場合、try-catch ではどうにもなりません。なぜなら、非同期コードが実行される頃には try-catch のコンテキストは終了しているからです。
3つ目は最も致命的です:React コンポーネントのレンダリングエラー。コンポーネントの return 文の中で undefined なプロパティにアクセスしてしまうと、ページは即座にホワイトアウトします。ここでも try-catch は全く役に立ちません。
React Error Boundary の仕組み
React はこの問題を早期に認識し、Error Boundary というメカニズムを導入しました。原理はシンプルです。コンポーネントツリーはマトリョーシカのようなもので、エラーは内側から外側へと「バブルアップ(泡のように上昇)」し、最も近い Error Boundary コンポーネントに到達するまで伝播します。
従来の方法では、クラスコンポーネントを書き、componentDidCatch と getDerivedStateFromError という2つのライフサイクルメソッドを実装する必要がありました。正直、毎回クラスコンポーネントを書くのは面倒です。それに、多くの人は今や関数コンポーネントに慣れており、これらは使えません。
Next.js の優雅な解決策
Next.js 13 で App Router が導入されてから、Error Boundary は驚くほど簡単にカプセル化されました。ルートディレクトリに error.tsx ファイルを作成するだけで、それが自動的にそのルートの Error Boundary になります。クラスコンポーネントを書く必要も、状態を自分で管理する必要もありません。Next.js がすべてやってくれます。
さらに重要な点があります。Next.js の Error Boundary は、サーバー側とクライアント側の両方のエラーを処理できます。Server Components がサーバーでのレンダリング中にエラーを出しても、最も近い error.tsx がそれを捕捉します。これは従来の React では不可能だったことです。
唯一の注意点は、error.tsx ファイル自体はクライアントコンポーネントでなければならず、ファイルの先頭に 'use client' マーカーが必要だということです。なぜなら、エラーステートの処理や回復ロジックに React Hooks を使う必要があり、Hooks はクライアントでしか動作しないからです。
Facebook Messenger は典型的な例です。サイドバー、チャットボックス、メッセージ入力エリアをそれぞれ別の Error Boundary で囲っています。あるエリアがクラッシュしても、他のエリアは通常通り機能します。ユーザーは問題が起きたことに気づかないことさえあるでしょう。
これこそが Error Boundary の核心的な価値です。局所的なエラーを全体的な災害にしないことです。
error.tsx の使い方 - 局所的な Error Boundary
さて、実践に移りましょう。error.tsx は具体的にどう書くのでしょうか?
基本構造:5分で実装
任意のルートディレクトリに error.tsx を作成し、以下のコードを貼り付けてください:
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Sentry などの監視プラットフォームにエラーを送信
console.error('エラーを捕捉しました:', error)
}, [error])
return (
<div className="flex flex-col items-center justify-center min-h-screen p-4">
<h2 className="text-2xl font-bold mb-4">おっと、問題が発生しました</h2>
<p className="text-gray-600 mb-4">
{error.message || 'ページの読み込みに失敗しました'}
</p>
<button
onClick={() => reset()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
再試行
</button>
</div>
)
}いくつかの重要ポイント:
- ‘use client’ は必須: これがないと Next.js はエラーを吐きます。
- error オブジェクト: エラーメッセージとスタックを含みます。Next.js 15 で追加された
digestフィールドはエラー追跡に使えます。 - reset 関数: クリックすると Error Boundary 内のコンテンツを再レンダリングし、ユーザーに自力での回復チャンスを与えます。
エラーのバブルアップメカニズム:エレベーターのように上昇
この仕組みは最初は少し混乱するかもしれません。ディレクトリ構造で説明しましょう:
app/
├── layout.tsx # ルートレイアウト
├── error.tsx # ルート以下のエラーを捕捉 (A)
├── page.tsx # ホームページ
├── dashboard/
│ ├── layout.tsx # dashboard レイアウト
│ ├── error.tsx # dashboard 以下のエラーを捕捉 (B)
│ └── page.tsx # dashboard ページ
└── profile/
└── page.tsx # profile ページもし dashboard/page.tsx のレンダリング中にエラーが起きたら、誰が捕捉するでしょうか? 答えは (B)、つまり最も近い親の error.tsx です。
では profile/page.tsx がエラーになったら? profile ディレクトリには error.tsx がないので、エラーはバブルアップし続け、(A) に捕捉されます。
注意すべき落とし穴:error.tsx は同じ階層の layout.tsx のエラーを捕捉できません。なぜなら、Error Boundary 自体が layout の中にラップされているからです。layout が壊れたら、Error Boundary もロードされません。dashboard/layout.tsx のエラーを捕捉したい場合は、app/error.tsx で処理する必要があります。
reset() の正しい使い方
reset 関数は魔法のように聞こえますが、実際にはエラーコンポーネントのサブツリーを再レンダリングしているだけです。以下のような一時的なエラーに有効です:
- API リクエストのタイムアウト(再試行で成功する可能性あり)
- ネットワークの揺らぎによるリソース読み込み失敗
- ユーザー入力が引き起こした境界条件
しかし、コードのバグ、例えば undefined.property にアクセスしているような場合は、何度「再試行」を押しても無駄です。その場合は、監視プラットフォームでエラーを確認し、コードを修正してリリースするしかありません。
reset ロジックにカウンターを追加し、3回以上失敗したら「再試行」ボタンを隠して「ページを更新するかお問い合わせください」と表示するチームもいます。これは非常に実用的です:
'use client'
import { useEffect, useState } from 'react'
export default function Error({ error, reset }: {
error: Error & { digest?: string }
reset: () => void
}) {
const [retryCount, setRetryCount] = useState(0)
const handleReset = () => {
setRetryCount(prev => prev + 1)
reset()
}
return (
<div>
<h2>エラーが発生しました</h2>
{retryCount < 3 ? (
<button onClick={handleReset}>
再試行 ({retryCount}/3)
</button>
) : (
<p>何度も失敗しました。ページを更新するか、<a href="/contact">お問い合わせ</a>ください。</p>
)}
</div>
)
}global-error.tsx - グローバルなエラーの最終防衛線
error.tsx は強力ですが、抜け穴があります。ルートレイアウト app/layout.tsx のエラーを捕捉できないのです。そこで global-error.tsx の出番です。
いつ global-error.tsx を使うべきか?
正直なところ、このファイルが本番環境で発動することは稀です。主に2つの壊滅的なシナリオを処理します:
- ルートの layout.tsx の初期化に失敗した(グローバル状態管理ライブラリが壊れたなど)
- どの error.tsx にも捕捉されなかった「漏れ」
私はこれを最後のセーフティネットだと考えています。使いたくはないですが、なければなりません。
global-error.tsx の特殊性
通常の error.tsx と異なり、global-error.tsx には決定的な違いがあります。それは、<html> と <body> タグを含む完全な HTML 構造を持たなければならないということです。
なぜなら、ルートの layout.tsx が壊れたということは、ページ全体のフレームワークが失われたことを意味するからです。global-error.tsx はゼロから最小限のページを構築しなければなりません。
完全なコードは以下のようになります:
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
padding: '20px',
fontFamily: 'system-ui, sans-serif'
}}>
<h1>アプリケーションに深刻な問題が発生しました</h1>
<p style={{ color: '#666', marginBottom: '20px' }}>
{process.env.NODE_ENV === 'development'
? error.message
: '現在問題を処理中です。しばらくしてからお試しください'}
</p>
<button
onClick={() => reset()}
style={{
padding: '10px 20px',
background: '#0070f3',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
アプリケーションを再読み込み
</button>
</div>
</body>
</html>
)
}Tailwind や CSS Modules ではなく、インラインスタイルを使っていることに気づきましたか? 理由は単純です。この段階ではスタイルシステムさえロードされていない可能性があるので、最も原始的な方法で表示を保証する必要があるからです。
開発環境 vs 本番環境
注意すべき詳細があります。global-error.tsx は本番環境でのみ有効です。開発環境では、Next.js はデバッグしやすいように赤いエラースタックページを表示し続けます。
本番環境では、技術的なエラー情報を隠し、ユーザーにフレンドリーなメッセージだけを見せるべきです。上記のコードにある process.env.NODE_ENV の判定はそのためのものです。ユーザーは「TypeError: Cannot read property ‘map’ of undefined」なんて気にしません。「使えるのか」「いつ直るのか」だけを知りたいのです。
global-error.tsx は必要か?
私のアドバイスは「イエス」です。発動確率は低いですが、一度起きれば大事故です。このバックアッププランがあれば、少なくともブラウザのデフォルトの「アクセスできません」画面ではなく、体裁の整ったエラーページを見せることができます。
保険のようなものです。事故は望みませんが、万が一のために加入しておいた方が良いでしょう。
Server Components のエラー処理における特別な考慮事項
Next.js 13以降の Server Components は、エラー処理に新たな課題をもたらしました。サーバー側とクライアント側のエラー処理は少し異なります。
Server Components のエラーはどこへ行く?
Server Components を扱い始めた頃、少し混乱しました。サーバーコンポーネントはサーバー上でレンダリングされますが、エラーが起きた場合、クライアントの error.tsx はそれを捕捉できるのでしょうか?
答えは「イエス」です。Next.js はサーバーのエラー情報をクライアントに転送し、最も近い error.tsx をトリガーします。しかし、重要なセキュリティメカニズムがあります。本番環境では、サーバーの機密情報が漏洩しないよう、エラー情報が秘匿化(脱感作)されます。
例えば、データベース接続の失敗は、開発環境では完全なスタックを表示しますが、本番環境のユーザーには「ロードに失敗しました」のような一般的なメッセージしか見えません。
予期されるエラー vs 予期しないエラー
この概念は重要で、公式ドキュメントでも強調されています。2種類のエラーを区別する必要があります:
予期されるエラー(Expected Errors): ビジネスロジックの範囲内のエラーで、明示的に処理すべきもの
- フォーム検証の失敗(ユーザー入力フォーマット不正)
- API が 404 を返す(データが存在しない)
- 権限不足(ユーザーがログインしていない)
予期しないエラー(Unexpected Errors): コードのバグやシステムレベルの例外で、Error Boundary に任せるべきもの
- データベース接続失敗
- サードパーティサービスのダウン
- コードが undefined なプロパティにアクセスした
予期されるエラーについては、Server Action やデータ取得関数内で try-catch を使って処理し、エラー情報をコンポーネントに返すのが最善です:
// app/actions.ts
'use server'
export async function createUser(formData: FormData) {
const email = formData.get('email') as string
// 予期されるエラー: メールアドレスの形式不正
if (!email.includes('@')) {
return { error: '有効なメールアドレスを入力してください' }
}
try {
await db.user.create({ email })
return { success: true }
} catch (error) {
// 予期しないエラー: DBダウンなど。Error Boundary に処理させるためにスローする
throw new Error('ユーザー作成に失敗しました')
}
}予期しないエラーについては、そのまま throw して、最も近い error.tsx にバブルアップさせます。
データ取得時のエラー処理
Server Components でのデータ取得では、通常このように処理します:
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
// 予期されるエラー: API がエラーステータスを返す
if (!res.ok) {
// エラータイプに応じて明示的に処理するかスローするか決める
if (res.status === 404) {
return { posts: [], error: 'データなし' }
}
// サーバーエラーならスローして Error Boundary に任せる
throw new Error('データ取得失敗')
}
return { posts: await res.json() }
}
export default async function PostsPage() {
const { posts, error } = await getPosts()
// エラーステートを明示的にレンダリング
if (error) {
return <div>記事がありません</div>
}
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
)
}この利点は、ユーザー体験がより良くなることです。「データなし」でエラーページをトリガーする必要はなく、真のシステムエラーだけが error.tsx のフォールバック UI を表示します。
error.digest の活用
Next.js 15 で error オブジェクトに digest フィールドが追加されました。これは自動生成される一意の識別子です。
何の役に立つのでしょうか? こんなシナリオを想像してください。ユーザーがエラーページを見て、サポートにスクリーンショットを送ってきました。「ページが開かない」。サポート担当者はこの digest を使ってログを検索し、どのリクエストで、いつ、どんなエラーが起きたかを正確に特定できます。
error.tsx ではこのように使えます:
'use client'
export default function Error({ error }: { error: Error & { digest?: string }}) {
return (
<div>
<h2>エラーが発生しました</h2>
<p>エラー番号: {error.digest}</p>
<p>サポートに連絡する際は、上記の番号をお伝えください</p>
</div>
)
}Sentry や他の監視プラットフォームと組み合わせれば、この digest によってエラー追跡の効率が数倍に跳ね上がります。
本番環境のベストプラクティス
使い方は説明しましたが、最後に「上手な使い方」を共有します。私の失敗経験から得た教訓です。
1. 粒度の細かい Error Boundary 設計
ルートディレクトリに error.tsx を1つ置いて終わりにしてはいけません。重要な機能エリアには個別に Error Boundary を設けるのがベストです。
例えば、ECサイトならこのように分けられます:
app/
├── error.tsx # 全体用
├── (shop)/
│ ├── products/
│ │ └── error.tsx # 商品リストのエラーが他に影響しない
│ ├── cart/
│ │ └── error.tsx # カートエラーが商品の閲覧を妨げない
│ └── checkout/
│ └── error.tsx # 決済フローは最重要、個別に処理こうすれば、カートコンポーネントがクラッシュしても、ユーザーは商品を見続けることができます。サイト全体が使えなくなることを防げます。
2. エラー監視と報告
error.tsx の useEffect は報告の絶好のタイミングです:
'use client'
import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'
export default function Error({ error, reset }: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Sentry に報告
Sentry.captureException(error, {
tags: {
errorDigest: error.digest,
errorBoundary: 'app-root'
},
extra: {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
}
})
}, [error])
return (
// エラー UI...
)
}error.digest とユーザー環境情報を含めるのを忘れずに。問題の再現に役立ちます。
私のチームでは、ユーザーの直近の操作パス(最後に訪れた5ページなど)も記録しており、問題解決に非常に役立っています。
3. ユーザーフレンドリーなエラーUI
エンジニアはスタックトレースを見たがりますが、ユーザーはそんなもの気にしません。彼らが知りたいのは:
- 何が起きた?(わかりやすい言葉で)
- 解決できる?(明確なアクションを提供)
- データは消えた?(影響範囲の説明)
良いエラーUIの例:
return (
<div className="error-container">
<h2>読み込みに失敗しました</h2>
<p>ネットワークが不安定か、サーバーが少し休憩中のようです</p>
<div className="actions">
<button onClick={reset}>もう一度試す</button>
<a href="/">トップページへ</a>
<a href="/help">サポートへ連絡</a>
</div>
<details className="error-details">
<summary>技術情報(オプション)</summary>
<code>{error.digest}</code>
</details>
</div>
)軽いトーンを使い、ユーザーの不安を和らげましょう。「500 Internal Server Error」より「サーバーが休憩中」の方がずっとフレンドリーです。
4. インテリジェントな再試行戦略
再試行回数の制限については前述しましたが、さらにいくつかのテクニックがあります:
- 遅延再試行: すぐに
resetせず、1〜2秒待ってサーバーに余裕を与える - 指数バックオフ: 1回目は1秒、2回目は2秒、3回目は4秒待つ
- エラータイプによる区別: ネットワークエラーは再試行を推奨、コードエラーはサポートへの連絡を表示
const [retryCount, setRetryCount] = useState(0)
const [isRetrying, setIsRetrying] = useState(false)
const handleReset = async () => {
setIsRetrying(true)
setRetryCount(prev => prev + 1)
// 指数バックオフ: 2^retryCount 秒待つ
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, retryCount) * 1000)
)
setIsRetrying(false)
reset()
}5. 環境別の処理
開発環境と本番環境でエラー表示を変えるべきです:
const isDev = process.env.NODE_ENV === 'development'
return (
<div>
<h2>{isDev ? error.message : '問題が発生しました'}</h2>
{isDev && (
<pre>
<code>{error.stack}</code>
</pre>
)}
{!isDev && (
<p>この問題は記録されました。早急に修正します。</p>
)}
</div>
)開発環境では完全なスタックでデバッグしやすく、本番環境ではフレンドリーなメッセージで技術詳細を漏らさないようにします。
6. 過度な使用を避ける
最後に1つ:Error Boundary はセーフティネットであり、主要なエラー処理手段ではありません。
try-catch で処理できる予期されるエラーを Error Boundary に投げないでください。コンポーネント内部で優雅に降格できるなら、エラーページをトリガーしないでください。
例えば、ユーザーアバターの読み込みに失敗したら、デフォルト画像を表示すればいいだけで、プロフィールページ全体をクラッシュさせる必要はありません。
Error Boundary は、真に予期せぬ、局所的に処理できないエラーのために取っておきましょう。
結論
長くなりましたが、要点は3つです:
第一に、Error Boundary はオプションではなく必須です。ホワイトスクリーンによるユーザー離脱は想像以上に深刻です。エラー境界を設定する時間を惜しまなければ、夜中に叩き起こされる事態を大幅に減らせます。
第二に、階層的な処理が鍵です。error.tsx で局所エラーを、global-error.tsx で全体を、Server Components では予期されるエラーと予期しないエラーを使い分ける。明示的に処理すべきところは処理し、Error Boundary に任せるところは任せる。
第三に、ユーザー体験を最優先に。技術的な詳細は監視プラットフォームに残し、ユーザーに見せるのは常にフレンドリーで操作可能なヒントです。「再試行」ボタンは40%の一時的エラーを解決できます。この投資対効果は非常に高いです。
今すぐ Next.js プロジェクトに error.tsx を追加しましょう。ルートディレクトリから始めて、重要な機能エリアに徐々に追加してください。Sentry などの監視ツールと組み合わせれば、アプリの安定性が目に見えて向上するはずです。
そして global-error.tsx も忘れずに。めったに発動しませんが、それはシートベルトのようなものです——使わずに済むことを祈りますが、なければ命取りになります。
Next.js で Error Boundary を実装する
Next.js アプリケーションにエラー境界を追加し、ランタイムエラーを優雅に処理する手順
- 1
Step1: error.tsx ファイルの作成
app ディレクトリまたは任意のルートディレクトリに error.tsx ファイルを作成し、'use client' ディレクティブを追加します。 - 2
Step2: エラー処理コンポーネントの実装
error と reset パラメータを受け取る Error コンポーネントを定義し、フレンドリーなエラー UI をデザインします。 - 3
Step3: エラー報告の追加
useEffect 内で Sentry などの監視プラットフォームにエラーを報告し、error.digest を記録します。 - 4
Step4: インテリジェントリトライの実装
リトライボタンを追加し、リトライ回数を制限して、一時的なエラーに対する自動回復メカニズムを提供します。 - 5
Step5: global-error.tsx の作成
app ディレクトリに global-error.tsx を作成し、最後のセーフティネットとして機能させます。完全な HTML 構造を含めます。 - 6
Step6: エラータイプの区別
Server Components では、予期されるエラー(明示的に処理)と予期しないエラー(Error Boundary に任せる)を区別します。
FAQ
error.tsx と global-error.tsx の違いは何ですか?
なぜ error.tsx はクライアントコンポーネントでなければならないのですか?
Server Components のエラーは error.tsx で捕捉できますか?
いつ try-catch を使い、いつ Error Boundary を使うべきですか?
reset() 関数はどのように動作しますか?
7 min read · 公開日: 2026年1月6日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます