0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · 课时

细粒度 error.tsx 与全局错误边界

在片段级和根级捕获渲染与操作失败,并通过可恢复的重置流程进行处理。

细粒度 error.tsx 与全局错误边界 是 CoddyKit 上的免费 Next.js 15 Fullstack (App Router + Server Actions) 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Next.js 15 Fullstack (App Router + Server Actions) 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Error Boundaries Matter in App Router

In Next.js 15's App Router, your application is composed of nested route segments. Each segment can independently fail during rendering or data fetching. Without proper boundaries, a single broken segment crashes the entire page — or worse, the entire application.

Next.js solves this with two special files:

  • error.tsx — catches errors within a specific route segment and its children
  • global-error.tsx — catches errors in the root layout, acting as a last-resort safety net

These boundaries give you granular control: a failure in /dashboard/analytics can be caught and recovered without taking down /dashboard/settings.

The error.tsx Contract

An error.tsx file must export a default React Client Component (it requires the 'use client' directive). It receives two props from Next.js automatically:

  • error — the thrown Error object, including a digest property (a server-generated hash for server-side error correlation)
  • reset — a function you call to re-render the segment, giving users a recovery path

The component is rendered in place of the segment that failed, wrapped in a React Error Boundary by the framework. You do not need to write the class-based boundary yourself.

'use client'

import { useEffect } from 'react'

interface ErrorPageProps {
  error: Error & { digest?: string }
  reset: () => void
}

export default function ErrorPage({ error, reset }: ErrorPageProps) {
  useEffect(() => {
    // Log to your observability service (e.g. Sentry, Datadog)
    console.error('[Segment Error]', error.message, 'digest:', error.digest)
  }, [error])

  return (
    <div role="alert" className="p-6 rounded-md border border-red-300 bg-red-50">
      <h2 className="text-lg font-semibold text-red-700">Something went wrong</h2>
      <p className="text-sm text-red-600 mt-1">{error.message}</p>
      <button
        onClick={reset}
        className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
      >
        Try again
      </button>
    </div>
  )
}

Placing error.tsx in the Right Segment

Error boundaries in App Router follow the folder hierarchy. An error.tsx placed in a folder catches errors thrown by the page.tsx and any child layouts within that folder — but not the layout at the same level.

This is a critical distinction: the layout wraps the error boundary, so layout errors bubble up to the parent segment's boundary.

Typical placement strategy:

  • app/error.tsx — catches errors in the root page and all segment pages (but not app/layout.tsx)
  • app/dashboard/error.tsx — catches only the dashboard subtree
  • app/dashboard/analytics/error.tsx — hyper-targeted to one leaf segment
// File tree showing boundary scopes:
//
// app/
// ├── layout.tsx          ← NOT caught by app/error.tsx
// ├── error.tsx           ← catches app/page.tsx failures
// ├── page.tsx
// └── dashboard/
//     ├── layout.tsx      ← NOT caught by dashboard/error.tsx
//     ├── error.tsx       ← catches dashboard subtree
//     ├── page.tsx
//     └── analytics/
//         ├── error.tsx   ← catches only this segment
//         └── page.tsx

// A page that deliberately throws to test the boundary:
export default async function AnalyticsPage() {
  const data = await fetch('/api/analytics')
  if (!data.ok) {
    // This error is caught by analytics/error.tsx
    throw new Error('Failed to load analytics data')
  }
  return <div>Analytics content</div>
}

The reset() Function and Recoverable Failures

The reset function passed to your error boundary attempts to re-render the failed segment without a full page reload. This is important for user experience: transient failures (network hiccups, momentary server unavailability) can often be resolved just by retrying.

However, reset() only re-renders the client-side React tree. For Server Components that fetch data, Next.js will also re-fetch server data as part of the reset cycle in Next.js 15.

Best practices for reset flows:

  • Show a clear, actionable error message — avoid raw error strings in production
  • Limit retry attempts to avoid infinite loops on permanent failures
  • Use router.refresh() from useRouter alongside reset() to invalidate the router cache when needed
'use client'

import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'

interface ErrorPageProps {
  error: Error & { digest?: string }
  reset: () => void
}

export default function ErrorPage({ error, reset }: ErrorPageProps) {
  const router = useRouter()
  const [retryCount, setRetryCount] = useState(0)
  const MAX_RETRIES = 3

  useEffect(() => {
    console.error('[Error boundary triggered]', {
      message: error.message,
      digest: error.digest,
      retryCount,
    })
  }, [error, retryCount])

  function handleReset() {
    if (retryCount >= MAX_RETRIES) return
    setRetryCount((c) => c + 1)
    router.refresh() // invalidate router cache
    reset()          // re-render the segment
  }

  return (
    <div role="alert">
      <p>An error occurred: {error.message}</p>
      {retryCount < MAX_RETRIES ? (
        <button onClick={handleReset}>Retry ({MAX_RETRIES - retryCount} left)</button>
      ) : (
        <p>Too many retries. Please <a href="/">return home</a>.</p>
      )}
    </div>
  )
}

global-error.tsx: The Root-Level Safety Net

global-error.tsx sits in the app/ directory and catches errors thrown by app/layout.tsx — the one place that regular error.tsx cannot reach. It is the absolute last line of defence.

Key differences from error.tsx:

  • It replaces the entire document when active, including the root layout. You must render your own <html> and <body> tags inside it.
  • It is only active in production builds. In development, Next.js shows its own overlay instead.
  • It still receives the same error and reset props.

Because it replaces the layout, keep it minimal but functional — include just enough structure to display a meaningful message and a recovery option.

'use client'

// app/global-error.tsx
// Catches errors thrown inside app/layout.tsx

interface GlobalErrorProps {
  error: Error & { digest?: string }
  reset: () => void
}

export default function GlobalError({ error, reset }: GlobalErrorProps) {
  return (
    // Must include <html> and <body> since root layout is bypassed
    <html lang="en">
      <body>
        <div
          style={{
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            minHeight: '100vh',
            fontFamily: 'sans-serif',
            padding: '2rem',
          }}
        >
          <h1 style={{ fontSize: '1.5rem', marginBottom: '0.5rem' }}>
            Application Error
          </h1>
          <p style={{ color: '#666', marginBottom: '1rem' }}>
            {process.env.NODE_ENV === 'production'
              ? 'An unexpected error occurred.'
              : error.message}
          </p>
          {error.digest && (
            <code style={{ fontSize: '0.75rem', color: '#999' }}>
              Error ID: {error.digest}
            </code>
          )}
          <button
            onClick={reset}
            style={{ marginTop: '1.5rem', padding: '0.5rem 1.5rem', cursor: 'pointer' }}
          >
            Reload Application
          </button>
        </div>
      </body>
    </html>
  )
}

Typed Errors and the digest Property

When a Server Component throws an error, Next.js does not expose the raw error message to the client in production. Instead, it generates a digest — a short hash string that uniquely identifies the server-side error. This prevents sensitive implementation details from leaking to users.

The digest appears in your server logs alongside the full error. By displaying the digest in your error UI, support teams can correlate user-reported issues with server logs without ever exposing stack traces.

Custom error classes let you pass structured metadata through your boundaries, but remember: only information that reaches the client error props is safe to display.

// lib/errors.ts — custom typed errors for structured handling

export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    public readonly isOperational: boolean = true,
  ) {
    super(message)
    this.name = 'AppError'
    // Maintains proper prototype chain in TypeScript
    Object.setPrototypeOf(this, AppError.prototype)
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 'NOT_FOUND', 404)
    this.name = 'NotFoundError'
  }
}

export class UnauthorizedError extends AppError {
  constructor() {
    super('Unauthorized access', 'UNAUTHORIZED', 401)
    this.name = 'UnauthorizedError'
  }
}

// Usage in a Server Component:
// app/posts/[id]/page.tsx
export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await db.post.findUnique({ where: { id: params.id } })
  if (!post) {
    throw new NotFoundError('Post') // caught by nearest error.tsx
  }
  return <article>{post.content}</article>
}

Catching Server Action Errors in error.tsx

Server Actions can throw errors, but the behavior differs depending on how the action is invoked:

  • Action called during initial render (e.g., inside a Server Component's async body) — the error propagates to the nearest error.tsx boundary automatically.
  • Action called from a form or event handler on the client — the error is not automatically caught by error.tsx. You must handle it in the component using try/catch or by returning structured error objects.

The recommended pattern is to never throw from Server Actions called client-side. Instead, return a discriminated union so the client can react gracefully without relying on error boundaries.

// app/actions/create-post.ts
'use server'

import { revalidatePath } from 'next/cache'
import { z } from 'zod'

const CreatePostSchema = z.object({
  title: z.string().min(3).max(100),
  content: z.string().min(10),
})

type ActionResult =
  | { success: true; postId: string }
  | { success: false; error: string; fieldErrors?: Record<string, string[]> }

export async function createPost(
  _prevState: ActionResult | null,
  formData: FormData,
): Promise<ActionResult> {
  const parsed = CreatePostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  })

  if (!parsed.success) {
    return {
      success: false,
      error: 'Validation failed',
      fieldErrors: parsed.error.flatten().fieldErrors,
    }
  }

  try {
    const post = await db.post.create({ data: parsed.data })
    revalidatePath('/posts')
    return { success: true, postId: post.id }
  } catch (err) {
    // Return error, don't throw — the client form handles this
    console.error('[createPost] DB error:', err)
    return { success: false, error: 'Failed to save post. Please try again.' }
  }
}

Using useActionState with Error Handling

Next.js 15 uses React 19's useActionState hook (formerly useFormState) to wire Server Actions to forms with built-in state management. This is the idiomatic way to surface Server Action errors in the UI without triggering error boundaries.

The hook returns a tuple of [state, dispatch, isPending]. The state reflects the last return value from your action — including any error payloads you return.

This pattern keeps the happy path, validation errors, and unexpected errors all in one cohesive component with no boundary involvement.

'use client'

import { useActionState } from 'react'
import { createPost } from '@/app/actions/create-post'

const initialState = null

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, initialState)

  return (
    <form action={formAction} className="space-y-4">
      {state && !state.success && (
        <div role="alert" className="p-3 bg-red-50 text-red-700 rounded">
          <p className="font-medium">{state.error}</p>
          {state.fieldErrors?.title && (
            <p className="text-sm">{state.fieldErrors.title[0]}</p>
          )}
        </div>
      )}

      {state?.success && (
        <p className="text-green-600">Post created! ID: {state.postId}</p>
      )}

      <div>
        <label htmlFor="title" className="block text-sm font-medium">
          Title
        </label>
        <input id="title" name="title" type="text" className="mt-1 w-full border rounded px-3 py-2" />
      </div>

      <div>
        <label htmlFor="content" className="block text-sm font-medium">
          Content
        </label>
        <textarea id="content" name="content" rows={4} className="mt-1 w-full border rounded px-3 py-2" />
      </div>

      <button
        type="submit"
        disabled={isPending}
        className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {isPending ? 'Saving...' : 'Create Post'}
      </button>
    </form>
  )
}

Integrating Sentry with error.tsx

Error boundaries are the natural integration point for observability tools like Sentry. The useEffect in your error boundary fires on every caught error, giving you a clean hook to report to external services.

For server-side errors in Server Components and Route Handlers, Next.js 15 supports Sentry's instrumentation via instrumentation.ts — a file at the project root that runs in the Node.js runtime on startup. Combined with client-side reporting in error boundaries, you get full-stack coverage.

Key considerations:

  • Never log raw user data or PII to external services
  • Use the digest as a correlation ID to link client-reported errors to server logs
  • Filter out expected operational errors (e.g., 404s, auth errors) to keep your alert noise low
'use client'

import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'

interface ErrorPageProps {
  error: Error & { digest?: string }
  reset: () => void
}

export default function ErrorPage({ error, reset }: ErrorPageProps) {
  useEffect(() => {
    // Capture with structured context for Sentry
    Sentry.captureException(error, {
      tags: {
        digest: error.digest ?? 'unknown',
        errorName: error.name,
      },
      // Avoid logging PII — only structural metadata
      extra: {
        digest: error.digest,
      },
    })
  }, [error])

  const isOperational = error.name === 'AppError'

  return (
    <div role="alert" className="p-6">
      <h2 className="text-xl font-bold">
        {isOperational ? error.message : 'An unexpected error occurred'}
      </h2>
      {error.digest && (
        <p className="text-sm text-gray-500 mt-1">
          Reference: <code>{error.digest}</code>
        </p>
      )}
      <button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">
        Try again
      </button>
    </div>
  )
}

Nested Boundaries: Granular Segment Isolation

One of the most powerful patterns in App Router is composing multiple error boundaries to isolate independent UI regions. A dashboard with a sidebar, main content, and a widget panel should have separate boundaries — a broken analytics widget should not kill the navigation.

You achieve this by placing error.tsx files at each relevant level of your route hierarchy, but you can also achieve intra-segment isolation using React's own ErrorBoundary class component or libraries like react-error-boundary for non-route UI regions.

// app/dashboard/layout.tsx
// The dashboard layout renders slots independently
// so each slot has its own error.tsx for isolation

import { Suspense } from 'react'
import { SidebarNav } from '@/components/SidebarNav'

export default function DashboardLayout({
  children,
  analytics,  // parallel route slot
  activity,   // parallel route slot
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  activity: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-[240px_1fr_320px] min-h-screen">
      {/* Sidebar: wrapped by its own Suspense + route-level error.tsx */}
      <aside className="border-r">
        <Suspense fallback={<div>Loading nav...</div>}>
          <SidebarNav />
        </Suspense>
      </aside>

      {/* Main content: errors caught by children segment's error.tsx */}
      <main className="p-6">{children}</main>

      {/* Right panel: each slot has its own @analytics/error.tsx */}
      <aside className="border-l p-4 space-y-4">
        <Suspense fallback={<div>Loading analytics...</div>}>
          {analytics}
        </Suspense>
        <Suspense fallback={<div>Loading activity...</div>}>
          {activity}
        </Suspense>
      </aside>
    </div>
  )
}

Production vs Development Error Exposure

Next.js intentionally behaves differently in development and production regarding error details:

  • Development — Full error overlays with stack traces are shown. global-error.tsx is not activated; the dev overlay takes priority. Error messages from Server Components are forwarded to the client in full.
  • Production — Server Component errors are sanitized. Only the digest is forwarded to the client. Your error.tsx will receive a generic message like 'An error occurred in the Server Components render...' with the digest attached.

This means your error boundary UI must account for two realities: rich messages in dev (great for debugging) and sanitized messages in production (critical for security). Always check process.env.NODE_ENV before rendering raw error messages.

'use client'

interface ErrorPageProps {
  error: Error & { digest?: string }
  reset: () => void
}

export default function ErrorPage({ error, reset }: ErrorPageProps) {
  const isDev = process.env.NODE_ENV === 'development'

  return (
    <div role="alert" className="p-6 border border-red-200 rounded bg-red-50 max-w-lg mx-auto mt-8">
      <h2 className="text-lg font-semibold text-red-800 mb-2">Something went wrong</h2>

      {/* In development, show the raw message for fast debugging */}
      {isDev && (
        <details className="mb-3">
          <summary className="text-sm text-red-600 cursor-pointer">Error details (dev only)</summary>
          <pre className="mt-2 text-xs text-red-700 overflow-auto p-2 bg-red-100 rounded">
            {error.message}
          </pre>
        </details>
      )}

      {/* In production, only show the digest for support correlation */}
      {!isDev && error.digest && (
        <p className="text-sm text-red-600 mb-3">
          Error code: <code className="font-mono">{error.digest}</code>
          <br />
          <span className="text-xs">Share this code with support if the issue persists.</span>
        </p>
      )}

      <button
        onClick={reset}
        className="px-4 py-2 bg-red-600 text-white rounded text-sm hover:bg-red-700"
      >
        Try again
      </button>
    </div>
  )
}

When Does an Error Bubble Past error.tsx?

Consider the following scenario in an App Router project:

A developer places an error.tsx inside app/dashboard/. An unhandled error is thrown inside app/dashboard/layout.tsx during rendering.

What happens to that error?

Lesson Recap: Error Boundaries in App Router

In this lesson you learned how to build robust, granular error handling in Next.js 15 App Router applications. Here is a summary of the key concepts:

  • error.tsx must be a Client Component ('use client') and receives error and reset props. It catches errors from page.tsx and child segments — but not from the layout at the same level.
  • global-error.tsx is the root-level safety net that catches errors from app/layout.tsx. It must render its own <html> and <body> and is only active in production.
  • The digest property on the error object is a server-generated hash that correlates client-visible errors with server logs without exposing sensitive details.
  • reset() re-renders the failed segment without a full page reload. Combine it with router.refresh() when you need to invalidate cached server data.
  • Server Actions called from client forms should return errors as structured data rather than throwing — use useActionState to wire these results to your UI.
  • Always guard raw error messages behind process.env.NODE_ENV === 'development' to prevent information leakage in production.

常见问题解答

「细粒度 error.tsx 与全局错误边界」课时是免费的吗?

是的 — 「细粒度 error.tsx 与全局错误边界」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Next.js 15 Fullstack (App Router + Server Actions) 课程的其余内容,请升级到 CoddyKit PRO。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。

「细粒度 error.tsx 与全局错误边界」这节课中我会学到什么?

在片段级和根级捕获渲染与操作失败,并通过可恢复的重置流程进行处理。 你通过在浏览器中直接运行的动手代码来练习 Next.js 15 Fullstack (App Router + Server Actions),全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Next.js 15 Fullstack (App Router + Server Actions) 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Next.js 15 Fullstack (App Router + Server Actions) 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「细粒度 error.tsx 与全局错误边界」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Next.js 15 Fullstack (App Router + Server Actions) 课中编写并运行代码吗?

能。每节 Next.js 15 Fullstack (App Router + Server Actions) 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 instrumentation.ts 进行 OpenTelemetry 追踪
  2. 细粒度 error.tsx 与全局错误边界
  3. 跨服务器与 Edge 的结构化日志记录
  4. 捕获服务器操作失败与遥测数据
← 返回 Next.js 15 Fullstack (App Router + Server Actions)