React ловит исключения в дереве компонентов через error boundary — это подробно разобрано в статье «Обработка ошибок: error boundary и Suspense». Но есть целый слой ошибок, которые React не видит вообще: асинхронный код вне компонентов, ошибки загрузки ресурсов, непойманные промисы. Их обрабатывают на уровне платформы — JavaScript и браузера.
try/catch и его пределы
try/catch перехватывает синхронные исключения — те, что выбрасываются прямо в текущем стеке вызовов:
try {
const data = JSON.parse(rawString);
} catch (err) {
console.error('Не удалось разобрать JSON:', err.message);
}
Проблема: try/catch не ловит ошибки в асинхронном коде, запущенном через setTimeout, setInterval или колбэк события.
try {
setTimeout(() => {
throw new Error('эта ошибка не поймается');
}, 0);
} catch (err) {
// сюда мы никогда не придём
}
К тому моменту, когда колбэк выполнится, стек try/catch уже завершился. Ошибка уйдёт в глобальный обработчик.
Ошибки в промисах
С Promise ситуация похожа: отклонённый промис без .catch или без await внутри try/catch становится «непойманным» — браузер генерирует событие unhandledrejection.
// правильно: явный .catch
fetch('/api/users')
.then(res => res.json())
.catch(err => console.error('Ошибка загрузки:', err));
// правильно: async/await + try/catch
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
return await res.json();
} catch (err) {
console.error('Ошибка:', err);
throw err; // пробрасываем дальше, не глотаем
}
}
async/await делает обработку ошибок промисов идентичной синхронной — try/catch работает как ожидается. Это и есть главная причина, по которой async/await вытеснил цепочки .then. Подробнее о том, почему это так работает, — в статье «Асинхронность и event loop».
Глобальные обработчики браузера
Когда ошибка не поймана локально, браузер генерирует события на window. Это точка входа для систем мониторинга.
window.onerror перехватывает необработанные синхронные исключения и ошибки в колбэках:
window.onerror = function(message, source, lineno, colno, error) {
sendToMonitoring({ message, source, lineno, colno, stack: error?.stack });
return true; // подавить вывод в консоли
};
unhandledrejection перехватывает непойманные отклонения промисов:
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault(); // подавить вывод в консоли
sendToMonitoring({ reason: event.reason });
});
Эти два обработчика вместе закрывают большую часть «утечек» ошибок. Их регистрируют в точке входа приложения — до монтирования React.
Ошибки загрузки ресурсов
Ошибки загрузки изображений, скриптов и стилей — отдельный случай. Они не всплывают как обычные события, поэтому нужна фаза захвата:
window.addEventListener('error', (event) => {
const target = event.target;
if (target instanceof HTMLImageElement || target instanceof HTMLScriptElement) {
sendToMonitoring({ type: 'resource', url: target.src || target.href });
}
}, true); // true = фаза захвата (capturing)
Без третьего аргумента true этот обработчик не получит события загрузки ресурсов: они не всплывают по дереву DOM.
Собственные классы ошибок
Встроенный Error несёт только сообщение и стек. Для разграничения ошибок по типу полезно создавать собственные классы. С ES2022 удобно использовать свойство cause, которое сохраняет оригинальную ошибку:
class ApiError extends Error {
constructor(
message: string,
public readonly statusCode: number,
options?: ErrorOptions
) {
super(message, options);
this.name = 'ApiError';
}
}
// использование
try {
const data = await loadData();
} catch (err) {
throw new ApiError('Не удалось загрузить данные', 500, { cause: err });
}
cause сохраняет оригинальную ошибку — стек не теряется, а при логировании видна вся цепочка причин.
Проверить тип ошибки можно через instanceof:
if (err instanceof ApiError && err.statusCode === 401) {
redirectToLogin();
}
Мониторинг в продакшене
Обработать ошибку локально — полдела. Если пользователь в продакшене видит сбой, разработчик должен об этом узнать. Для этого ошибки отправляют в систему мониторинга (Sentry, Rollbar или собственный эндпоинт).
Ключевая проблема в продакшене — минификация. После сборки стек ошибки выглядит так:
TypeError: Cannot read properties of undefined
at e (app.a8f3c2.js:1:4821)
app.a8f3c2.js:1:4821 ни о чём не говорит. Source maps — это файлы, которые бандлер генерирует во время сборки и которые связывают минифицированный код с исходным. Мониторинговый сервис загружает их и восстанавливает читаемый стек:
TypeError: Cannot read properties of undefined
at loadUserProfile (src/features/profile/ProfilePage.tsx:42:18)
Source maps не стоит выкладывать публично — они раскрывают исходный код. Обычно их загружают напрямую в систему мониторинга через CI и удаляют из публичных ресурсов.
Чего не делать
Глотать ошибки. Пустой catch — один из самых дорогостоящих дефектов: ошибка исчезает, приложение работает некорректно, а причина недоступна. Залогировать и не перебросить — почти то же самое: вызывающий код не знает об ошибке.
// плохо: ошибка исчезла бесследно
try { await saveData(); } catch (e) {}
// хорошо: логируем и пробрасываем
try {
await saveData();
} catch (e) {
logger.error('Ошибка сохранения данных', e);
throw e;
}
Полагаться только на console.error. В продакшене никто не смотрит консоль пользователя. Все нестандартные ошибки должны попадать в систему мониторинга.
Не разграничивать ошибки по типу. Если все ошибки обрабатываются одинаково, невозможно отличить «сеть упала» от «баг в коде» — а это принципиально разные реакции.
Коротко
try/catchперехватывает только синхронные исключения и код сawait; колбэкиsetTimeoutон не ловит.- В промисах:
.catchдля цепочек;try/catchвокругawait— дляasync/await. window.onerror+unhandledrejection— глобальная сеть в точке входа приложения, закрывает то, что не поймали локально.- Ошибки загрузки ресурсов (
<img>,<script>) приходят вaddEventListener('error', ..., true)— фаза захвата обязательна. - Собственные классы (
extends Error,cause) дают тип и контекст без потери стека. - В продакшене без source maps стек нечитаем — загружайте их в мониторинг, не публикуйте.
- Пустой
catchхуже, чем отсутствиеcatch: ошибка исчезает, источник невозможно найти.
Что почитать дальше
- Обработка ошибок: error boundary и Suspense — как React перехватывает исключения рендера.
- Асинхронность и event loop — почему
try/catchне ловит колбэки и как устроен event loop. - Тестирование frontend: Vitest и Testing Library — как тестировать пути ошибок в компонентах.