Ошибки в асинхронном коде
Ошибки в асинхронном коде
Технический фундамент распространения ошибок
В асинхронном коде ошибка проходит по цепочке вверх до ближайшего обработчика. Важно понимать:
rejectв Promise-цепочке эквивалентен "брошенной" ошибке для следующего шага;throwвнутриasyncвозвращает rejected Promise;- потеря контекста в
catchделает диагностику дорогой; - иногда ошибку нужно обработать частично и пробросить дальше (
rethrow).
Фундаментальный принцип: у каждой ошибки должен быть наблюдаемый маршрут и явное решение на каждом уровне.
Почему асинхронные ошибки сложнее обычных
В синхронном коде падение обычно видно сразу: строка, тип ошибки, стек. В асинхронном потоке ошибка может проявиться позже, в другом месте цепочки, и без корректной обработки превратиться в "тихий" баг или UnhandledPromiseRejection.
Ключевой момент: каждая асинхронная операция должна иметь явный путь обработки ошибки.
Смотри, что важно: try...catch ловит только ошибки, которые происходят внутри текущего синхронного выполнения. Если throw случится позже (например, в setTimeout), внешний try...catch его не поймает.
try {
setTimeout(() => {
throw new Error('boom');
}, 0);
} catch (error) {
console.log('не попадем сюда');
}
Анти-провал: ловить/превращать ошибку нужно внутри асинхронного обработчика (или переводить сценарий в Promise и работать через catch/await).
Проверь себя: что опаснее - явный rejected, который поймали, или rejected, о котором никто не узнал?
Ошибки в Promise-цепочке
Если Promise отклоняется (reject) и нет catch, ошибка остается необработанной.
function loadData() {
return Promise.reject(new Error('Сервер недоступен'));
}
loadData()
.then((data) => console.log(data))
.catch((error) => {
console.error('Ошибка загрузки:', error.message);
});
Здесь часто путаются: ошибка может появиться не только в самом запросе, но и внутри любого then.
Promise.resolve({ price: null })
.then((item) => item.price.toFixed(2))
.catch((error) => console.log('Поймали:', error.message));
Ошибки в async/await
С async/await основной паттерн обработки - try...catch.
async function getProducts() {
try {
const response = await fetch('/api/products');
if (!response.ok) throw new Error('HTTP error');
return await response.json();
} catch (error) {
console.error('getProducts failed:', error.message);
return [];
}
}
Смотри, что важно: сетевой fetch не бросает ошибку на HTTP 404/500 автоматически, поэтому проверка response.ok обязательна.
Смотри, что важно: try...catch в async-функции ловит только то, что ты await-ишь. Если забыть await, ошибка может стать необработанной.
async function save() {
try {
apiSave(); // забыли await
console.log('считаем, что сохранили...');
} catch (error) {
// сюда не попадем при rejected из apiSave()
}
}
Анти-провал: если хочешь обработать ошибку здесь, пиши await apiSave(). Если хочешь пробросить ошибку наружу, делай return apiSave() и лови ее у вызывающего.
Проверь себя: почему catch может не сработать на 404 без if (!response.ok) throw ...?
Глобальные ловушки и почему на них нельзя полагаться
В Node.js и браузере есть глобальные обработчики необработанных ошибок, но это "последняя линия защиты", а не основной способ.
Примеры:
- браузер:
window.addEventListener('unhandledrejection', ...),window.addEventListener('error', ...); - Node.js:
process.on('unhandledRejection', ...),process.on('uncaughtException', ...).
Почему:
- трудно восстановить контекст конкретной операции;
- часть состояния уже могла испортиться;
- UX ухудшается, потому что ошибка обрабатывается слишком поздно.
Анти-провал: обрабатывай ошибки как можно ближе к месту, где операция запускается.
Мини-сценарии из продукта
- Повторная попытка (retry) при временном сетевом сбое.
- первая попытка упала;
- показываем пользователю статус "повторяем";
- пробуем еще раз ограниченное число раз;
- если не помогло, выдаем понятную ошибку.
- Частичный fallback.
- основной API упал;
- показываем кешированные данные;
- логируем инцидент для команды.
Это лучше, чем пустой экран или бесконечный spinner.
Частые ошибки новичков
- Забывать
catchвPromise-цепочке. - Ловить ошибку, но ничего не делать (нет лога, нет fallback).
- Смешивать бизнес-ошибки и технические в одно сообщение.
- Использовать слишком общий
catchбез контекста.
async function save() {
try {
await apiSave();
} catch {
// Плохо: потеряли причину
}
}
Лучше всегда иметь доступ хотя бы к error.message и контексту операции.
Что будет, если изменить входные данные
Если item.price в примере с toFixed будет числом, цепочка пройдет успешно. Если null или undefined, возникнет ошибка типа, и она уйдет в catch. Это предсказуемое поведение, если обработка выстроена правильно.
Проверь себя: как ты отделишь ошибку валидации пользовательского ввода от ошибки сети в одном обработчике?
Практичный чеклист для асинхронной ошибки
- Есть ли
catchилиtry...catchу каждой операции? - Логируется ли контекст (
operation, вход, message)? - Есть ли понятный fallback для пользователя?
- Проверяешь ли ты
response.okдля HTTP-ответов? - Не теряешь ли оригинальную ошибку при пробросе дальше?
Дополнительный пример: добавление контекста и повторный проброс.
async function loadWithContext() {
try {
return await loadData();
} catch (error) {
console.error('loadWithContext failed:', error.message);
throw error;
}
}
Краткий итог
- Асинхронные ошибки нужно обрабатывать явно в каждой точке запуска операции.
- В
Promise-цепочках за это отвечаетcatch, вasync/await-try...catch. - Для HTTP важно проверять
response.ok, иначе часть ошибок останется незамеченной. - Лог + контекст + fallback делают поведение системы устойчивым.
- Необработанные
Promise-ошибки это сигнал архитектурной дыры, а не мелочь.