Стандарты
Error Handling
Стандарт обработки ошибок — классификация, middleware, логирование
Error Handling
Принцип
Ошибки классифицируются, обрабатываются централизованно и никогда не замалчиваются. Пользователь получает понятное сообщение, разработчик — полный контекст.
Приоритет: P0 — обязательно для backend и frontend.
Классификация ошибок
| Тип | HTTP код | Действие | Пример |
|---|---|---|---|
| Validation | 400 | Показать пользователю, какие поля невалидны | Некорректный email |
| Authentication | 401 | Перенаправить на логин | Протухший токен |
| Authorization | 403 | Показать "Нет доступа" | Чужой ресурс |
| Not Found | 404 | Показать "Не найдено" | Несуществующий ID |
| Conflict | 409 | Объяснить конфликт | Email уже занят |
| Internal | 500 | Логировать, показать "Что-то пошло не так" | Сбой БД |
Backend: AppError
Все бизнес-ошибки выбрасываются как AppError — кастомный класс с кодом и HTTP-статусом.
// server/utils/errors.ts
export class AppError extends Error {
constructor(
public readonly code: string,
message: string,
public readonly statusCode: number = 400,
public readonly details?: Record<string, unknown>,
) {
super(message)
this.name = 'AppError'
}
}
// Фабрики для частых случаев
export function notFound(entity: string, id?: string): AppError {
const msg = id ? `${entity} с id ${id} не найден` : `${entity} не найден`
return new AppError(`${entity.toUpperCase()}_NOT_FOUND`, msg, 404)
}
export function conflict(message: string): AppError {
return new AppError('CONFLICT', message, 409)
}
export function forbidden(message = 'Нет доступа'): AppError {
return new AppError('FORBIDDEN', message, 403)
}
Использование в сервисах
// server/services/task.service.ts
import { notFound, forbidden } from '~/server/utils/errors'
export const TaskService = {
async update(id: string, userId: string, input: UpdateTaskInput) {
const task = await prisma.task.findUnique({ where: { id } })
if (!task) throw notFound('Task', id)
if (task.ownerId !== userId) throw forbidden('Только автор может редактировать задачу')
return prisma.task.update({ where: { id }, data: input })
},
}
Backend: Global Error Handler
Centralized middleware перехватывает все ошибки и формирует единый формат ответа.
// server/middleware/error-handler.ts
export default defineEventHandler((event) => {
event.context._onError = true
})
// server/plugins/error-handler.ts
import { AppError } from '~/server/utils/errors'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('error', (error, { event }) => {
if (!event) return
if (error instanceof AppError) {
setResponseStatus(event, error.statusCode)
return send(event, JSON.stringify({
error: {
code: error.code,
message: error.message,
...(error.details && { details: error.details }),
},
}), 'application/json')
}
// Zod validation errors
if (error.name === 'ZodError') {
setResponseStatus(event, 400)
return send(event, JSON.stringify({
error: {
code: 'VALIDATION_ERROR',
message: 'Ошибка валидации',
details: error.issues,
},
}), 'application/json')
}
// Prisma known errors
if (error.code === 'P2025') {
setResponseStatus(event, 404)
return send(event, JSON.stringify({
error: {
code: 'NOT_FOUND',
message: 'Запись не найдена',
},
}), 'application/json')
}
// Unexpected errors — логируем полностью, отдаём минимум
console.error('[UNHANDLED]', error)
setResponseStatus(event, 500)
return send(event, JSON.stringify({
error: {
code: 'INTERNAL_ERROR',
message: 'Внутренняя ошибка сервера',
},
}), 'application/json')
})
})
Backend: Формат ответа об ошибке
Все ошибки API возвращаются в едином формате:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "Пользователь с id abc123 не найден",
"details": {}
}
}
Поле details опционально — используется для ошибок валидации (список полей) или для дополнительного контекста.
Frontend: Обработка ошибок API
Composable для API-вызовов
// app/composables/useApiError.ts
interface ApiError {
code: string
message: string
details?: Record<string, unknown>
}
export function useApiError() {
const toast = useToast()
function handleError(error: unknown) {
const apiError = extractApiError(error)
if (apiError.code === 'VALIDATION_ERROR') {
// Валидационные ошибки обрабатываются формой
return apiError
}
// Остальные — toast-уведомление
toast.add({
title: 'Ошибка',
description: apiError.message,
color: 'error',
})
return apiError
}
return { handleError }
}
function extractApiError(error: unknown): ApiError {
if (error && typeof error === 'object' && 'data' in error) {
const data = (error as any).data
if (data?.error) return data.error
}
return { code: 'UNKNOWN', message: 'Произошла непредвиденная ошибка' }
}
Обработка в компонентах
<script setup lang="ts">
const { handleError } = useApiError()
async function onSubmit(data: CreateUserInput) {
try {
await useAPI('/api/users', { method: 'POST', body: data })
navigateTo('/users')
} catch (error) {
handleError(error)
}
}
</script>
Серверные ошибки валидации в формах
Когда сервер возвращает ошибки валидации, они отображаются на соответствующих полях:
<script setup lang="ts">
const serverErrors = ref<Record<string, string>>({})
async function onSubmit(data: CreateUserInput) {
serverErrors.value = {}
try {
await useAPI('/api/users', { method: 'POST', body: data })
} catch (error) {
const apiError = extractApiError(error)
if (apiError.code === 'VALIDATION_ERROR' && apiError.details) {
// Маппим серверные ошибки на поля формы
for (const issue of apiError.details as any[]) {
serverErrors.value[issue.path.join('.')] = issue.message
}
}
}
}
</script>
Frontend: Error Boundaries
Nuxt предоставляет error.vue для глобальных ошибок и <NuxtErrorBoundary> для локальных.
<!-- app/error.vue — глобальная страница ошибки -->
<template>
<UContainer>
<UPageError
:status="error?.statusCode || 500"
:name="error?.statusCode === 404 ? 'Страница не найдена' : 'Ошибка'"
:message="error?.message || 'Что-то пошло не так'"
/>
</UContainer>
</template>
<script setup lang="ts">
defineProps<{ error: { statusCode: number; message: string } }>()
</script>
Правила
Что делать
- Выбрасывай
AppErrorв сервисах — с кодом, сообщением и HTTP-статусом - Валидируй входные данные на границе (хэндлер) через Zod
- Логируй unexpected errors с полным стеком
- Показывай пользователю понятные сообщения на русском
- Используй toast-уведомления для операционных ошибок
- Используй error pages для навигационных ошибок (404, 500)
Чего не делать
- Не замалчивай ошибки —
catch {}без обработки - Не показывай пользователю stack trace или внутренние детали
- Не используй
createErrorв сервисах — это HTTP-специфичная функция Nitro - Не дублируй обработку ошибок — один global handler на сервере
- Не используй magic strings для кодов ошибок — используй константы
Чек-лист
- Все бизнес-ошибки —
AppErrorс кодом и статусом - Global error handler перехватывает все ошибки в едином формате
- Zod errors автоматически конвертируются в
VALIDATION_ERROR - Prisma P2025 автоматически конвертируется в 404
- Unexpected errors логируются, но пользователь видит generic сообщение
- Frontend показывает ошибки через toast или error page
- Серверные ошибки валидации маппятся на поля формы
Связанные документы
- Standard: Service Layer — где выбрасывать ошибки
- Standard: Backend API — REST API конвенции
- Standard: Forms & Validation — валидация на клиенте