Даже опытные разработчики сталкиваются с коварной проблемой: тихая смерть приложения из-за неперехваченной асинхронной ошибки. Картина знакома: код работает при идеальных условиях, но падает без логов или вменяемых диагностических сообщений при реальных сбоях сети, невалидных ответах API или неожиданных исключениях. Почему стандартные методы терпят неудачу и как построить устойчивую систему?
Пределы try/catch
в асинхронном мире
Классическая конструкция try/catch
беспомощна против ошибок в асинхронных операциях вне своего лексического контекста. Пример-убийца:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json(); // Ошибка: response не получен
return data;
} catch (err) {
console.error('Локальный перехват:', err);
}
}
// Вызов функции НЕ защищен try/catch
const result = fetchData();
result.then(processData); // Непойманный rejected promise!
Здесь ошибка в fetchData()
приведёт к отклонению промиса, который обрабатывается в then
. Отсутствие обработчика на этом уровне вызовет UnhandledPromiseRejection
.
Слои обороны: архитектура устойчивости
- Глобальные ловцы асинхронных крахов
Регистрируйте обработчики на уровне процесса/браузера:
// Node.js
process.on('unhandledRejection', (reason, promise) => {
logger.critical('Неперехваченный промис:', reason);
// Мягкий restart сервиса и отчёт в Sentry
});
// Браузер
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault();
telemetry.trackError(event.reason);
});
Это последний рубеж, но он не заменяет локальную обработку. Отсутствие деталей контекста усложняет диагностику.
- Декларативная обработка
Promise
: токсичные антипаттерны
Типичная ошибка:
getUserData()
.then(updateUI)
.catch(err => console.log(err)); // Поглощение ошибки без действий!
Решение? Всегда возвращайте результат обработки. Используйте цепочки с интеллектом:
fetchConfig()
.then(validateConfig)
.then(initApp)
.catch(err => {
if (err instanceof NetworkError) showOfflineBanner();
else if (err.name === 'ValidationError') restoreBackup();
else throw err; // Проброс нефатальных ошибок дальше
});
- Обработаете все при "параллельных" операциях
Promise.all
рухнет при первой ошибке. Для независимых задач:
// Сохраняет все результаты (including ошибки)
const results = await Promise.allSettled([
fetchOrders(),
fetchUsers(),
fetchInventory()
]);
const errors = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (errors.length > 0) {
reportToMonitoring(errors);
}
Для кворума успешных ответов (Promise.any
) или первого успеха после выполненных (Promise.race
с таймаутами) используйте аналогичные тактики изолирования ошибок.
- Структурные паттерны для сложных приложений
А. Обёртки для контролируемых сайд-эффектов
async function safeAsync<T>(
fn: () => Promise<T>,
ctx?: string
): Promise<[T, null] | [null, Error]> {
try {
const data = await fn();
return [data, null];
} catch (err) {
err.context = ctx; // Обогащение ошибки
return [null, err];
}
}
// Использование
const [user, error] = await safeAsync(() => fetchUser(uid), 'UserProfile');
if (error) {
if (error.context === 'UserProfile') handleProfileError();
}
Б. Функциональные pipelines с обработкой
Композиция асинхронных шагов с монолитической обработкой ошибок:
const processOrder = pipeAsync(
fetchOrder,
validatePayment, // Нарушение валидации остановит весь pipe
updateInventory,
sendConfirmation
).catch(err => rollbackTransaction(err));
- Интеграция с фреймворками
React: Используйте Error Boundaries исключительно для событий рендеринга. Для обработки асинхронных ошибок в эффектах:
useEffect(() => {
fetchData()
.then(setData)
.catch(err => setError(err)); // Контроллируемый стейт ошибки
}, []);
Node.js: Middleware с централизованной ошибкой в Express/Koa:
// Koa
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.isCustom ? 400 : 500;
ctx.body = { error: err.message };
// Пуш в централизованный логгер
}
});
Диагностика ошибок как часть дизайна
- Добавляйте уникальные коды ошибок:
throw new Error('USER_FETCH_FAILED: DB timeout')
- Прикрепляйте контекст: идентификаторы запросов, параметры вызова, состояние приложения
- Разделение фатальных и нефотальных сбоев: ReservedDisconnect можно игнорировать, а DatabaseFailure транслируем.
Инструменты: OpenTelemetry для трассировки, структурированные логи через Pino/Bunyan, Sentry для фронтенд-ошибок.
Не позволяйте асинхронным ошибкам подрывать систему в темноте. Архитектура устойчивости — это не добавление случайных catch
на глаз, а дерзкий дизайн потоков данных и ошибок. Расширяйте оборону от unhandledrejection
до каждого кастомного промиса, срабатывающего в вашем коде. Мёртвые промисы должны оставлять кристально чистый след из логов — тогда даже ошибки становятся упорядоченными событиями жизненного цикла приложения, а не причинами хаоса.