Обробка помилок у Next.js
Обробка помилок у Next.js App Router - спеціальні файли (error.tsx, not-found.tsx, global-error.tsx) автоматично створюють React Error Boundaries на кожному рівні сегмента маршруту, щоб збій в одному місці не руйнував весь застосунок.
Теорія
TL;DR
- Кожен сегмент маршруту може мати власний
error.tsx- помилка в/dashboard/usersпоказує UI помилки тільки цього сегмента, навігація й інші секції залишаються на місці - Аналогія: автоматичні вимикачі в будинку - вибитий запобіжник на кухні не вимикає світло у всіх кімнатах
error.tsxперехоплює runtime-помилки;not-found.tsxвідповідає за відсутні ресурси (404)error.tsxзавжди має бути клієнтським компонентом ('use client') - без виключень- Помилки піднімаються вгору: якщо в поточному сегменті немає
error.tsx, Next.js шукає у батьківському, потім у кореневому
Швидкий приклад
// app/dashboard/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Дашборд не завантажився</h2>
<p>{error.message}</p>
{/* digest - хеш для серверного логування, безпечно показувати */}
<details>ID помилки: {error.digest}</details>
{/* reset() перерендерить тільки цей сегмент */}
<button onClick={reset}>Спробувати ще раз</button>
</div>
)
}Поклади цей файл у app/dashboard/ - і будь-яка помилка всередині /dashboard або його дочірніх маршрутів покаже цей UI. Кореневий layout (навігація, бічна панель) залишається незміненим.
Головна різниця
error.tsx створює межу помилки (Error Boundary) на рівні сегмента. Помилка в app/blog/[slug]/page.tsx активує тільки app/blog/error.tsx або підніметься до кореневого, але не покаже білий екран на весь застосунок. У Pages Router одна помилка могла стерти весь shell. Ще одна деталь: error.tsx не перехоплює помилки з layout.tsx того самого сегмента, бо межа помилки вкладена всередину layout. Для помилок layout потрібен error.tsx у батьківському сегменті або global-error.tsx.
Коли що використовувати
- Runtime-помилка при завантаженні даних або в компоненті:
error.tsxу тій самій папці - Відсутній ресурс на динамічному маршруті (
/posts/123, де поста немає): викликnotFound()зnext/navigation, а неthrow new Error(...)- так повернеться правильний статус 404 - Помилки в кореневому layout (auth-провайдер, тема):
app/global-error.tsx - Помилки в Server Actions (форми, мутації): повертай
{ error: '...' }з екшену, не кидай виключень -error.tsxїх не перехоплює - Помилки в API-роутах (
app/api/route.ts): стандартнийtry/catchвсередині хендлера,error.tsxїх не зачіпає
global-error.tsx
Коли помилка виникає в кореневому layout, звичайний 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 + body - цей компонент повністю замінює кореневий layout
<html>
<body>
<h2>Щось пішло не так</h2>
<button onClick={reset}>Перезавантажити</button>
</body>
</html>
)
}У режимі розробки Next.js показує власний overlay помилок замість цього компонента. Кастомний global-error.tsx видно тільки в production-збірці.
Порядок обробки
layout.tsx
└── error.tsx (перехоплює помилки з дочірніх компонентів)
└── loading.tsx
└── not-found.tsx
└── page.tsxПомилка з page.tsx піднімається до найближчого error.tsx. Якщо в цьому сегменті його немає, іде до батьківського. Якщо нічого не перехопило - застосунок повертає 500 або спрацьовує global-error.tsx.
Як це працює всередині
Next.js при білді та SSR сканує файлову систему і огортає дерево компонентів сегмента в React Error Boundary там, де є error.tsx. Коли відбувається throw, React перехоплює його через механізм, схожий на componentDidCatch, і перерендерює тільки поддерево цього сегмента з UI помилки. Функція reset запускає оновлення стану, яке змушує React повторити рендер оригінального дерева без повної навігації.
На сервері error.digest - це хеш стек-трейсу. Клієнт отримує тільки цей хеш, а не саму трасу, тому внутрішні деталі сервера залишаються захищеними. Хеш передають у систему логування (Sentry, Datadog, Vercel Logs) для трасування.
Server Actions
Server Actions повертають помилки інакше, ніж UI-компоненти. error.tsx їх не перехоплює, тому стандартний підхід - повертати об'єкт з помилкою:
'use server'
export async function updateProfile(formData: FormData) {
try {
await db.user.update({ /* ... */ })
revalidatePath('/settings')
return { success: true }
} catch (error) {
return { error: 'Не вдалося оновити профіль' }
}
}Компонент, який викликає екшен, перевіряє повернене значення та показує свій стан помилки.
Типові помилки
Забуто 'use client' у error.tsx:
// Неправильно - Next.js кине помилку збірки
export default function Error({ error }: any) {
return <div>{error.message}</div>
}Error Boundaries потребують внутрішнього управління станом і не можуть бути Server Components. 'use client' обов'язковий завжди.
throw new Error(...) замість notFound() для відсутніх даних:
// Неправильно - покаже UI краша, поверне статус 500
if (!post) throw new Error('Post not found')
// Правильно - активує not-found.tsx, поверне статус 404
import { notFound } from 'next/navigation'
if (!post) notFound()Статус 500 для відсутнього поста шкодить SEO. Пошукові системи по-різному трактують 404 і 500.
Локальний стан навколо reset():
// Проблемно - reset() повністю перемонтовує сегмент, стан зникає
const [retries, setRetries] = useState(0)
<button onClick={() => { setRetries(x => x + 1); reset() }}>Retry</button>reset() замінює дерево компонентів сегмента, знищуючи весь локальний стан. Для логування використовуй error.digest, не намагайся відстежувати кількість повторів.
Немає кореневого error.tsx:
На практиці це найпоширеніша проблема - команди пропускають кореневий рівень захисту, поки щось не впаде в production. Завжди додавай app/error.tsx і app/global-error.tsx як фінальний рівень. Це те саме, що catch у самому верхньому рівні застосунку.
Очікування, що error.tsx перехопить помилки layout того самого сегмента:
app/dashboard/
layout.tsx <- помилка тут
error.tsx <- НЕ перехопить цеМежа вкладена всередину layout і не може перехопити його власні помилки. Перенеси error.tsx на рівень вище або обробляй помилку безпосередньо в layout.tsx.
Реальне використання
- Vercel Commerce: сегментний
error.tsxдля краша checkout,error.digestпередається у Vercel Logs - T3 Stack (tRPC + NextAuth):
app/(auth)/error.tsxперехоплює помилки tRPC-запитів, зберігаючи shell застосунку - Адмін-панелі на Shadcn/Tremor:
admin/[team]/error.tsxізолює помилки даних кожного тенанта - Payload CMS: кастомний
error.tsxз передачею digest у внутрішній вебхук
Follow-up питання
Q: Що станеться, якщо сам error.tsx кине виключення?
A: Воно підніметься до найближчого батьківського error.tsx. React має ліміт вкладеності меж, тому врешті-решт дійде до global-error.tsx або дефолтного обробника фреймворку.
Q: Як error.digest допомагає в production без витоку серверних деталей?
A: Next.js генерує хеш повного стек-трейсу на сервері і надсилає клієнту тільки цей хеш. У моніторинговому інструменті (Sentry, Vercel Logs) хеш пов'язує клієнтську помилку з повним серверним трейсом.
Q: Яка різниця між error.tsx і класовим React ErrorBoundary?
A: Next.js автоматично огортає сегменти на основі наявності файлу. Класова межа потребує ручного розміщення навколо кожного ризикового компонента. Крім того, reset() у Next.js перезапитує дані через стан навігації, а не просто перерендерює.
Q: Як паралельні маршрути впливають на підняття помилок?
A: Помилка в слоті паралельного маршруту (наприклад, @modal) залишається в цьому слоті. Якщо у слота немає error.tsx, вона підніметься до групового layout, і reset() там перезапустить всю паралельну групу. Додай error.tsx на рівні слота для ізоляції.
Q: (Senior) Як Next.js під час SSR обробляє серверні помилки, не витікаючи стек-трейс на клієнт?
A: Під час рендерингу RSC, якщо виникає throw, Next.js рендерить fallback межі помилки на сервері і надсилає клієнту очищений RSC payload. Клієнт гідратується з межею вже в стані помилки. Digest-хеш пов'язує client-видимий ID помилки з повним серверним трейсом у системі логування.
Приклади
Базовий: перехоплення помилок на динамічному маршруті товару
// app/shop/[id]/page.tsx
import { notFound } from 'next/navigation'
export default async function ProductPage({
params,
}: {
params: { id: string }
}) {
const id = parseInt(params.id)
// Цей throw перехоплює app/shop/error.tsx
if (isNaN(id)) throw new Error('Invalid product ID')
const product = await fetchProduct(id)
// Активує not-found.tsx з правильним статусом 404
if (!product) notFound()
return <div>{product.name}</div>
}// app/shop/error.tsx
'use client'
export default function ShopError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Не вдалося завантажити товар</h2>
<button onClick={reset}>Спробувати ще раз</button>
</div>
)
}При відкритті /shop/abc показується UI помилки, основна навігація залишається. /shop/999 (валідний ID, товару немає) повертає сторінку not-found зі статусом 404.
Середній: production-компонент помилки з логуванням у Sentry
// app/admin/users/error.tsx
'use client'
import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'
export default function AdminUsersError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// digest пов'язує цей лог з повним серверним трейсом у Sentry
Sentry.captureException(error, {
tags: { segment: 'admin-users', digest: error.digest },
})
}, [error])
return (
<div className="p-8">
<h2 className="text-xl font-bold text-red-600">Помилка адмін-панелі</h2>
<p className="mt-2 text-sm text-gray-500">ID помилки: {error.digest}</p>
<button
onClick={reset}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
>
Повторити
</button>
</div>
)
}useEffect запускається при монтуванні і одразу відправляє помилку в Sentry. Тег digest дозволяє знайти повний стек-трейс у дашборді.
Просунутий: Server Action з поверненням стану помилки
// app/settings/actions.ts
'use server'
export async function updateProfile(formData: FormData) {
try {
const name = formData.get('name') as string
await db.user.update({ where: { id: session.userId }, data: { name } })
revalidatePath('/settings')
return { success: true }
} catch {
return { error: 'Не вдалося оновити профіль. Спробуй ще раз.' }
}
}// app/settings/page.tsx
'use client'
import { updateProfile } from './actions'
import { useState } from 'react'
export default function SettingsForm() {
const [errorMsg, setErrorMsg] = useState<string | null>(null)
async function handleSubmit(formData: FormData) {
const result = await updateProfile(formData)
if (result.error) setErrorMsg(result.error)
}
return (
<form action={handleSubmit}>
<input name="name" />
{errorMsg && <p className="text-red-500">{errorMsg}</p>}
<button type="submit">Зберегти</button>
</form>
)
}Server Actions існують поза межами error.tsx. Повернення { error: '...' } замість виключення дає UI-компоненту повний контроль над відображенням проблеми.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.