Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Обробка помилок у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Обробка помилок у Next.js** побудована на файлах `error.tsx`, які створюють React Error Boundary для кожного сегмента маршруту. Файл перехоплює runtime-помилки, показує резервний UI і залишає батьківські layouts незміненими. Для відсутніх ресурсів використовуй `notFound()` (статус 404), для помилок кореневого layout - `global-error.tsx`. ```tsx 'use client' export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { return <button onClick={reset}>Повторити - ID: {error.digest}</button> } ``` **Ключове:** `error.tsx` завжди є клієнтським компонентом; помилки піднімаються до найближчого батьківського `error.tsx`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Обробка помилок у 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-компоненту повний контроль над відображенням проблеми.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.