言語を切り替える
テーマを切り替える

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 コンポーネントに到達するまで伝播します。

従来の方法では、クラスコンポーネントを書き、componentDidCatchgetDerivedStateFromError という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>
  )
}

いくつかの重要ポイント:

  1. ‘use client’ は必須: これがないと Next.js はエラーを吐きます。
  2. error オブジェクト: エラーメッセージとスタックを含みます。Next.js 15 で追加された digest フィールドはエラー追跡に使えます。
  3. 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>
  )
}
40%
再試行ボタンを提供することで、約40%の一時的エラーが自動的に回復します

global-error.tsx - グローバルなエラーの最終防衛線

error.tsx は強力ですが、抜け穴があります。ルートレイアウト app/layout.tsx のエラーを捕捉できないのです。そこで global-error.tsx の出番です。

いつ global-error.tsx を使うべきか?

正直なところ、このファイルが本番環境で発動することは稀です。主に2つの壊滅的なシナリオを処理します:

  1. ルートの layout.tsx の初期化に失敗した(グローバル状態管理ライブラリが壊れたなど)
  2. どの 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.tsxuseEffect は報告の絶好のタイミングです:

'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. 1

    Step1: error.tsx ファイルの作成

    app ディレクトリまたは任意のルートディレクトリに error.tsx ファイルを作成し、'use client' ディレクティブを追加します。
  2. 2

    Step2: エラー処理コンポーネントの実装

    error と reset パラメータを受け取る Error コンポーネントを定義し、フレンドリーなエラー UI をデザインします。
  3. 3

    Step3: エラー報告の追加

    useEffect 内で Sentry などの監視プラットフォームにエラーを報告し、error.digest を記録します。
  4. 4

    Step4: インテリジェントリトライの実装

    リトライボタンを追加し、リトライ回数を制限して、一時的なエラーに対する自動回復メカニズムを提供します。
  5. 5

    Step5: global-error.tsx の作成

    app ディレクトリに global-error.tsx を作成し、最後のセーフティネットとして機能させます。完全な HTML 構造を含めます。
  6. 6

    Step6: エラータイプの区別

    Server Components では、予期されるエラー(明示的に処理)と予期しないエラー(Error Boundary に任せる)を区別します。

FAQ

error.tsx と global-error.tsx の違いは何ですか?
error.tsx はルートセグメントレベルのエラーを捕捉しますが、同階層の layout.tsx のエラーは捕捉できません。global-error.tsx は最後の手段で、ルート layout.tsx のエラーを捕捉でき、完全な html と body タグを含む必要があり、本番環境でのみ有効です。
なぜ error.tsx はクライアントコンポーネントでなければならないのですか?
error.tsx はエラーステートの処理や回復ロジックに React Hooks(useEffect など)を使用する必要があり、Hooks はクライアントコンポーネントでのみ使用可能だからです。そのためファイルの先頭に 'use client' が必要です。
Server Components のエラーは error.tsx で捕捉できますか?
はい。Next.js はサーバーのエラー情報をクライアントに転送し、最も近い error.tsx をトリガーします。ただし、本番環境では機密情報漏洩防止のため、エラー情報は秘匿化されます。
いつ try-catch を使い、いつ Error Boundary を使うべきですか?
予期されるビジネスエラー(フォーム検証失敗、API 404、権限不足など)は try-catch で明示的に処理すべきです。Error Boundary は予期しないエラー(コードのバグ、DB接続失敗、サードパーティサービスのダウンなど)のために残しておくべきです。
reset() 関数はどのように動作しますか?
reset() はエラー境界内のコンポーネントサブツリーを再レンダリングします。これは一時的なエラー(ネットワークタイムアウト、リソース読み込み失敗)には有効ですが、コードのバグに対しては効果がなく、その場合はコード修正とデプロイが必要です。

7 min read · 公開日: 2026年1月6日 · 更新日: 2026年1月22日

コメント

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

関連記事