Skip to main content

Обробка помилок у 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 шукає у батьківському, потім у кореневому

Швидкий приклад

tsx
// 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:

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 їх не перехоплює, тому стандартний підхід - повертати об'єкт з помилкою:

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:

tsx
// Неправильно - Next.js кине помилку збірки export default function Error({ error }: any) { return <div>{error.message}</div> }

Error Boundaries потребують внутрішнього управління станом і не можуть бути Server Components. 'use client' обов'язковий завжди.

throw new Error(...) замість notFound() для відсутніх даних:

tsx
// Неправильно - покаже 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():

tsx
// Проблемно - 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 помилки з повним серверним трейсом у системі логування.

Приклади

Базовий: перехоплення помилок на динамічному маршруті товару

tsx
// 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> }
tsx
// 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

tsx
// 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 з поверненням стану помилки

tsx
// 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: 'Не вдалося оновити профіль. Спробуй ще раз.' } } }
tsx
// 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-компоненту повний контроль над відображенням проблеми.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?