Пошук уроків, статей та іншого контенту
Налаштуйте перехоплення, класифікацію та передачу помилок до системи моніторингу.
У production недостатньо показати користувачу сторінку помилки. Потрібно також:
перехопити помилку в потрібному місці;
визначити її категорію;
додати контекст: маршрут, джерело, ідентифікатор запиту;
передати дані до системи моніторингу;
не розкрити користувачу внутрішні деталі застосунку.
У Next.js помилки можуть виникати в різних середовищах:
у Client Components;
під час рендерингу Server Components;
у Route Handlers;
під час виконання Server Actions;
у middleware;
у layout або page.
Тому одного глобального try...catch недостатньо.
Перед передаванням помилки до моніторингу корисно поділити її на категорії:
validation — некоректні вхідні дані;
authentication — користувач не автентифікований;
authorization — користувач не має доступу;
not_found — ресурс не знайдено;
network — помилка зовнішнього сервісу або мережі;
unknown — непередбачена помилка.
Класифікація допомагає:
фільтрувати шум;
налаштовувати різні рівні сповіщень;
швидше знаходити першопричину;
будувати статистику за типами помилок.
Не варто класифікувати помилку лише за її текстом. Повідомлення можуть змінюватися. Надійніше використовувати name, HTTP-статус або спеціальні властивості помилки.
Створимо модуль lib/monitoring.ts. Він:
перетворює будь-яке значення на об'єкт Error;
визначає категорію;
формує структурований payload;
передає його до внутрішнього API або зовнішньої системи моніторингу.
// lib/monitoring.ts
export type ErrorSource =
| "client"
| "server-component"
| "route-handler"
| "server-action"
| "middleware";
export type ErrorCategory =
| "validation"
| "authentication"
| "authorization"
| "not_found"
| "network"
| "unknown";
export type ErrorContext = {
source: ErrorSource;
path?: string;
method?: string;
digest?: string;
requestId?: string;
extra?: Record<string, unknown>;
};
type MonitoringPayload = {
message: string;
name: string;
stack?: string;
category: ErrorCategory;
context: ErrorContext;
timestamp: string;
};
function toError(value: unknown): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
return new Error("Невідома помилка");
}
function getStatus(value: unknown): number | undefined {
if (typeof value !== "object" || value === null) {
return undefined;
}
const candidate = value as {
status?: unknown;
statusCode?: unknown;
};
if (typeof candidate.status === "number") {
return candidate.status;
}
if (typeof candidate.statusCode === "number") {
return candidate.statusCode;
}
return undefined;
}
export function classifyError(value: unknown): ErrorCategory {
const error = toError(value);
const status = getStatus(value);
if (status === 400 || error.name === "ValidationError") {
return "validation";
}
if (status === 401 || error.name === "AuthenticationError") {
return "authentication";
}
if (status === 403 || error.name === "AuthorizationError") {
return "authorization";
}
if (status === 404 || error.name === "NotFoundError") {
return "not_found";
}
if (
error.name === "FetchError" ||
error.name === "NetworkError" ||
error.name === "AbortError"
) {
return "network";
}
return "unknown";
}
export async function reportError(
value: unknown,
context: ErrorContext,
): Promise<void> {
const error = toError(value);
const payload: MonitoringPayload = {
message: error.message,
name: error.name,
stack: error.stack,
category: classifyError(value),
context,
timestamp: new Date().toISOString(),
};
try {
if (typeof window !== "undefined") {
await fetch("/api/monitoring/errors", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
keepalive: true,
});
return;
}
const monitoringUrl = process.env.MONITORING_URL;
if (!monitoringUrl) {
console.error("[monitoring]", payload);
return;
}
const response = await fetch(monitoringUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(process.env.MONITORING_TOKEN
? {
Authorization: `Bearer ${process.env.MONITORING_TOKEN}`,
}
: {}),
},
body: JSON.stringify(payload),
cache: "no-store",
});
if (!response.ok) {
console.error(
"[monitoring] Система моніторингу повернула помилку:",
response.status,
);
}
} catch (reportingError) {
// Помилка моніторингу не повинна ламати основний запит.
console.error("[monitoring] Не вдалося передати помилку", reportingError);
}
}Змінні середовища можуть мати такий вигляд:
MONITORING_URL=https://monitoring.example.com/events
MONITORING_TOKEN=secret-tokenУ реальному проєкті URL і формат payload залежать від конкретної системи моніторингу. Важливий сам принцип: застосунок формує структуровану подію, а не просто записує рядок у консоль.
Route Handler має самостійно перехоплювати помилки, які виникають під час обробки HTTP-запиту.
// app/api/orders/route.ts
import { reportError } from "@/lib/monitoring";
export async function POST(request: Request) {
try {
const body = await request.json();
if (typeof body.productId !== "string") {
const error = new Error("Поле productId є обов'язковим");
error.name = "ValidationError";
throw error;
}
// Тут зазвичай виконується запис замовлення до бази даних.
const order = {
id: crypto.randomUUID(),
productId: body.productId,
};
return Response.json(order, { status: 201 });
} catch (error) {
await reportError(error, {
source: "route-handler",
path: "/api/orders",
method: "POST",
});
return Response.json(
{
error: "Не вдалося створити замовлення",
},
{ status: 500 },
);
}
}Для очікуваних помилок можна повертати точніший HTTP-статус. Наприклад, помилка валідації повинна мати статус 400, а відсутність автентифікації — 401.
Водночас внутрішні повідомлення помилок не слід повертати клієнту:
return Response.json(
{
error: "Не вдалося створити замовлення",
},
{ status: 500 },
);Так користувач отримує зрозумілу відповідь, а деталі залишаються в системі моніторингу.
Для помилок під час рендерингу сегмента маршруту використовується файл error.tsx.
Файл розміщується поруч із page.tsx або layout.tsx:
app/
dashboard/
error.tsx
page.tsx// app/dashboard/error.tsx
"use client";
import { useEffect } from "react";
import { reportError } from "@/lib/monitoring";
type DashboardErrorProps = {
error: Error & { digest?: string };
reset: () => void;
};
export default function DashboardError({
error,
reset,
}: DashboardErrorProps) {
useEffect(() => {
void reportError(error, {
source: "client",
path: "/dashboard",
digest: error.digest,
});
}, [error]);
return (
<main>
<h1>Не вдалося завантажити панель керування</h1>
<p>Спробуйте повторити операцію.</p>
<button type="button" onClick={reset}>
Спробувати ще раз
</button>
</main>
);
}error.tsx є Client Component, тому в ньому доступні React-хуки та браузерний fetch.
Функція reset повторно намагається відрендерити відповідний сегмент маршруту. Це корисно, якщо помилка була тимчасовою, наприклад через короткочасний збій зовнішнього API.
Якщо помилка не була оброблена локальним error.tsx, можна використати app/global-error.tsx.
// app/global-error.tsx
"use client";
import { useEffect } from "react";
import { reportError } from "@/lib/monitoring";
type GlobalErrorProps = {
error: Error & { digest?: string };
reset: () => void;
};
export default function GlobalError({
error,
reset,
}: GlobalErrorProps) {
useEffect(() => {
void reportError(error, {
source: "client",
path: "global",
digest: error.digest,
});
}, [error]);
return (
<html lang="uk">
<body>
<main>
<h1>Сталася непередбачена помилка</h1>
<button type="button" onClick={reset}>
Спробувати ще раз
</button>
</main>
</body>
</html>
);
}global-error.tsx замінює весь кореневий layout під час відображення помилки, тому він повинен містити html і body.
Клієнтський код не повинен напряму містити секретний токен системи моніторингу. Тому браузер передає подію до внутрішнього Route Handler, а вже сервер надсилає її до зовнішнього сервісу.
// app/api/monitoring/errors/route.ts
import { reportError } from "@/lib/monitoring";
type ClientErrorPayload = {
message?: unknown;
name?: unknown;
stack?: unknown;
category?: unknown;
context?: {
source?: unknown;
path?: unknown;
digest?: unknown;
};
};
export async function POST(request: Request) {
try {
const body = (await request.json()) as ClientErrorPayload;
if (typeof body.message !== "string") {
return Response.json(
{ error: "Некоректний формат помилки" },
{ status: 400 },
);
}
const error = new Error(body.message);
if (typeof body.name === "string") {
error.name = body.name;
}
if (typeof body.stack === "string") {
error.stack = body.stack;
}
await reportError(error, {
source:
body.context?.source === "client"
? "client"
: "client",
path:
typeof body.context?.path === "string"
? body.context.path
: undefined,
digest:
typeof body.context?.digest === "string"
? body.context.digest
: undefined,
});
return Response.json({ accepted: true }, { status: 202 });
} catch {
return Response.json(
{ error: "Не вдалося обробити подію" },
{ status: 400 },
);
}
}Статус 202 Accepted означає, що подія прийнята до обробки. Клієнту не потрібно чекати повного завершення роботи зовнішньої системи.
У production цей endpoint потрібно додатково захистити від зловживань:
обмежити розмір тіла запиту;
застосувати rate limiting;
не приймати довільні службові поля без перевірки;
не зберігати персональні дані без потреби.
Мінімальний payload зазвичай містить:
повідомлення помилки;
назву помилки;
stack trace;
категорію;
джерело помилки;
URL або маршрут;
HTTP-метод;
timestamp;
digest, якщо його надав Next.js;
ідентифікатор запиту або користувача, якщо це дозволено політикою приватності.
digest є корисним ідентифікатором помилки, який Next.js може показувати замість внутрішніх деталей. Його слід зберігати разом із подією, щоб зіставляти повідомлення користувача з записом у логах.
Не передавайте без фільтрації:
паролі;
токени;
cookies;
повні заголовки авторизації;
платіжні дані;
великі тіла запитів;
персональні дані, які не потрібні для діагностики.
Перед передаванням події можна створити окрему функцію санітизації, яка видаляє секретні поля з extra.
Одна й та сама помилка може бути перехоплена на кількох рівнях:
Route Handler записав її до моніторингу;
помилка піднялася до error.tsx;
error.tsx записав її повторно.
Щоб уникнути дублювання:
визначте відповідальність кожного рівня;
серверні помилки записуйте на сервері;
у error.tsx записуйте помилки, які виникли під час клієнтського рендерингу;
використовуйте requestId, digest або fingerprint для об'єднання однакових подій.
Не потрібно обробляти помилку в кожному компоненті, якщо її вже перехоплює найближчий error.tsx.
Погано:
return Response.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 500 },
);Так можна випадково розкрити SQL-помилку, URL внутрішнього сервісу або службову інформацію.
Краще повертати стабільне публічне повідомлення, а оригінальну помилку передавати до моніторингу.
Значення, доступні клієнтському коду, не є секретними. Не передавайте токен моніторингу через змінну з префіксом NEXT_PUBLIC_.
Клієнт повинен звертатися до внутрішнього endpoint, а секретний токен має використовуватися лише на сервері.
await для передавання помилки на серверіПогано:
reportError(error, {
source: "route-handler",
});Якщо обробка запиту завершиться раніше, передавання події може бути перерване. У серверному коді використовуйте:
await reportError(error, {
source: "route-handler",
});У клієнтському useEffect можна використати void, якщо помилка моніторингу вже обробляється всередині reportError:
void reportError(error, {
source: "client",
});Система моніторингу не повинна ставати критичною залежністю для основного запиту. Якщо сервіс моніторингу недоступний:
не замінюйте початкову помилку помилкою надсилання;
запишіть проблему локально;
поверніть користувачу стандартну відповідь;
за потреби повторіть надсилання асинхронно.
Повідомлення Database error майже не допомагає. Додавайте щонайменше джерело, маршрут і категорію помилки.
Перехоплюйте серверні помилки в Route Handlers та Server Actions.
Використовуйте error.tsx для помилок під час рендерингу сегментів маршруту.
Використовуйте global-error.tsx як глобальний резервний error boundary.
Класифікуйте помилки за стабільними ознаками: статусом і назвою.
Передавайте до моніторингу структуровані події з контекстом.
Не передавайте секрети та персональні дані без необхідності.
Не показуйте користувачу внутрішні повідомлення помилок.
Помилка системи моніторингу не повинна ламати основний функціонал застосунку.
Враховуйте можливе дублювання подій і використовуйте digest або requestId для їх об'єднання.