В предыдущей главе разобрались, что fetch кидает исключение только при сетевых проблемах. Сейчас разберём, как такие исключения выглядят и как их обрабатывать — в одной обёртке try/catch вместе с собственными throw на HTTP-ошибки.
Что прилетает в catch
В catch попадает три разных типа ошибок — и полезно различать, какая чья.
1. Сетевая ошибка от fetch — TypeError
Если HTTP-ответа вообще не было, fetch отвергает Promise с TypeError и сообщением вроде «Failed to fetch» или «NetworkError when attempting to fetch resource»:
try {
await fetch('https://this-domain-does-not-exist.example/');
} catch (error) {
// error instanceof TypeError
// error.message === "Failed to fetch" (Chrome)
// === "NetworkError when attempting to fetch resource." (Firefox)
}
Конкретный текст зависит от браузера. По нему ориентироваться нельзя.
2. Наш собственный throw на HTTP-ошибке
Это то, что мы сами кидаем после проверки response.ok:
try {
const response = await fetch('/api/posts/9999');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
// ...
} catch (error) {
// error.message === "HTTP 404"
}
Тут уже мы сами контролируем текст. Можно кинуть свой объект ошибки с дополнительными полями.
3. Ошибка парсинга response.json()
Если в теле приехал не валидный JSON (HTML-страница ошибки сервера, обрезанный ответ), .json() кинет SyntaxError:
try {
const response = await fetch('/api/posts/1');
const data = await response.json(); // ← может упасть здесь
} catch (error) {
// error instanceof SyntaxError
// error.message === "Unexpected token < in JSON at position 0"
}
Все три типа попадают в один и тот же catch. Если нужно отличать — можно по instanceof или по флагу, который мы сами повесили на throw.
Свой класс ошибки — чище код
Когда обработка ошибок становится сложнее простого «показать сообщение», удобно завести свой класс:
class ApiError extends Error {
constructor(status, statusText, body) {
super(`HTTP ${status} ${statusText}`);
this.name = 'ApiError';
this.status = status;
this.body = body;
}
}
async function api(url, options) {
let response;
try {
response = await fetch(url, options);
} catch (error) {
// Сюда попали — TypeError. Сеть отвалилась.
throw new Error('Нет связи с сервером');
}
if (!response.ok) {
// Попробуем достать тело ошибки.
let body = null;
try {
body = await response.json();
} catch {}
throw new ApiError(response.status, response.statusText, body);
}
return response.json();
}
Теперь снаружи можно различать:
try {
await api('/api/posts/1');
} catch (error) {
if (error instanceof ApiError) {
if (error.status === 401) redirectToLogin();
else if (error.status === 429) showRateLimitMessage();
else showError(`Ошибка сервера: ${error.message}`);
} else {
showError('Проверьте подключение к интернету');
}
}
Что показывать пользователю
Универсальное правило: пользователь должен понять, что делать дальше.
- Сетевая ошибка — «нет связи с сервером, проверьте интернет».
- 4xx — чаще всего проблема в вводе или сессии: «проверьте поля формы» / «войдите снова». Конкретный текст обычно есть в теле ошибки — об этом следующая глава.
- 5xx — «что-то пошло не так у нас, попробуйте через минуту». Подробности не показываем, в логи — полную ошибку.
- SyntaxError при парсинге — обычно симптом 5xx (сервер вернул HTML-ошибку вместо JSON). Сообщение — то же «что-то пошло не так».
Никаких «TypeError: Cannot read properties of undefined (reading title)» на экране — это сообщение для разработчика, не для пользователя.
Лесенка ошибок
Если ваше приложение растёт, обработку ошибок имеет смысл собрать в один глобальный обработчик. Например, в React это ErrorBoundary, в Vue — errorCaptured, в чистом JS — window.addEventListener('unhandledrejection', ...). Чтобы случайно непойманная ошибка из fetch хотя бы попадала в систему мониторинга, а не молча терялась.
В следующей главе — как извлекать из тела ответа полезное сообщение для пользователя, а не показывать абстрактное «HTTP 422».