Когда сервер возвращает ошибку, он обычно прикладывает в теле какие-то подробности: что именно пошло не так, какое поле невалидно, как это исправить. Их полезно достать и показать пользователю — иначе на каждый 422 будет выскакивать абстрактное «что-то пошло не так», и пользователь не поймёт, что делать.
Самый частый формат
Чаще всего тело ошибки выглядит так:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"message": "Email уже занят",
"code": "EMAIL_TAKEN"
}
Достать — обычным response.json():
const response = await fetch('/api/users', { method: 'POST', body: JSON.stringify(data) });
if (!response.ok) {
const error = await response.json();
showError(error.message); // "Email уже занят"
return;
}
Но у каждого API формат свой. Чтобы не сюрпризовать пользователя, проверяем структуру:
if (!response.ok) {
let message = `Ошибка ${response.status}`; // fallback
try {
const body = await response.json();
if (body.message) message = body.message;
else if (body.error) message = body.error;
else if (typeof body === 'string') message = body;
} catch {
// тело не JSON — ничего страшного, оставим fallback
}
showError(message);
return;
}
Валидационные ошибки на форму
Когда невалидны несколько полей формы, сервер обычно присылает их списком:
HTTP/1.1 422 Unprocessable Entity
{
"message": "Validation failed",
"errors": {
"email": ["Не похоже на email"],
"password": ["Минимум 8 символов", "Должен содержать цифру"]
}
}
Удобный паттерн — раздать ошибки по полям формы:
if (!response.ok) {
const body = await response.json();
if (body.errors) {
for (const [field, messages] of Object.entries(body.errors)) {
const input = document.querySelector(`[name="${field}"]`);
const helper = input?.nextElementSibling;
if (helper) helper.textContent = messages.join('. ');
}
} else {
showError(body.message ?? 'Ошибка сервера');
}
return;
}
Конкретные ключи (errors, violations, fieldErrors) различаются от API к API. Прочитайте документацию или сделайте один тестовый запрос с заведомо невалидными данными — и посмотрите, что приходит.
RFC 7807 — стандартный формат проблем
Был придуман специальный стандарт — Problem Details for HTTP APIs (RFC 7807). Сервер возвращает Content-Type: application/problem+json с такой структурой:
{
"type": "https://api.example.com/errors/email-taken",
"title": "Email уже занят",
"status": 422,
"detail": "Пользователь с email anna@example.com уже зарегистрирован",
"instance": "/api/users"
}
На практике встречается редко — чаще API катят свой формат. Но если попался сервис, который придерживается RFC 7807 — ключи title и detail идеально ложатся в «заголовок и подробность» для тоста с ошибкой.
Когда тело — не JSON
Старые сервера, прокси-страницы Cloudflare, ошибки на уровне инфраструктуры — иногда вместо JSON возвращают HTML или plain-text. Попытка response.json() упадёт с SyntaxError.
Защититься можно так:
const contentType = response.headers.get('Content-Type') || '';
let body;
if (contentType.includes('application/json')) {
body = await response.json();
} else {
body = await response.text(); // HTML или plain — как строка
}
Логи такого ответа полезно собирать в систему мониторинга: 9 из 10 раз это симптом, что что-то сломалось не в самом API, а на уровне инфраструктуры.
Полная схема в одном куске кода
async function api(url, options) {
let response;
try {
response = await fetch(url, options);
} catch {
throw new Error('Нет связи с сервером');
}
const contentType = response.headers.get('Content-Type') || '';
const isJson = contentType.includes('application/json');
if (!response.ok) {
let body = null;
if (isJson) {
try { body = await response.json(); } catch {}
}
const message = body?.message ?? body?.error ?? `HTTP ${response.status}`;
throw new ApiError(response.status, message, body);
}
return isJson ? response.json() : response.text();
}
Эта схема покрывает большинство практических кейсов и аккуратно отделяет «сообщение для пользователя» от «что вообще произошло».
В последней главе модуля разберём, как отменять зависшие запросы и делать таймауты — ещё одна сторона надёжной работы с сетью.