Асинхронный код на JavaScript претерпел эволюцию от ада колбэков через чистилище промисов к элегантности async/await. Но за кажущейся простотой синтаксиса скрывается коварная ловушка: незаметное проглатывание исключений. Ваш код не падает, приложение выглядит работоспособным, а критические ошибки тихо исчезают в пустоте. Рассмотрим реальные сценарии и стратегии противодействия.
Проблемный паттерн: Неявное поглощение исключений
// Проблемный код: ошибка не обрабатывается!
async function processOrder(orderId) {
const order = await fetchOrder(orderId); // Может выбросить ошибку
updateInventory(order.items); // Может выбросить ошибку
await sendConfirmationEmail(order.user); // Может выбросить ошибку
}
// Где-то в другом месте:
processOrder(123); // Ошибка исчезнет без следа
Почему это фатально:
- Исключения в
processOrder
преобразуются в отклоненные промисы - Вызов функции без обработки
catch
приводит к необработанной ошибке промиса - В Node.js это завершит процесс
- В браузерах пользователь увидит разбитую логику без диагностики
- Нет журналирования, нет отката транзакций, нет уведомлений
Стратегия 1: Явная перехват с помощью try/catch
Базовый, но надежный метод:
async function createUser(userData) {
try {
const validation = validateUserData(userData); // Синхронная ошибка
const dbResponse = await db.insert(userData); // Асинхронная ошибка
await queueWelcomeEmail(userData.email);
} catch (error) {
// Комплексное журналирование с контекстом
logger.error('User creation failed', {
error,
userData: redactSensitiveFields(userData)
});
// Передаем адаптированную ошибку для middleware
throw new AppError('User creation failed', {
cause: error,
code: 'USER_CREATION_FAILURE'
});
}
}
Критичные нюансы:
- Синхронные ошибки внутри
async
функций генерируют отклоненные промисы автоматически - return из блока catch вернет успешное разрешение промиса (часто антипаттерн)
- Протоколирование в catch должно быть неблокирующим: используйте асинхронные логгеры или очередь сообщений
- Всегда конвертируйте в типизированные ошибки: библиотечные ошибки не имеют бизнес-контекста
Стратегия 2: Глобальные обработчики — ответственность джентльмена
// Node.js
process.on('unhandledRejection', (reason, promise) => {
telemetry.captureException(reason);
logger.fatal('Uncaught async error', { reason });
// Не принуждаем к завершению в продакшене - оставляем возможность восстановления
});
// Браузер
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault();
monitoring.report(event.reason);
});
Но ограниченно надежно:
- Невозможно применить восстановление контекста вызова
- Нет информации о состоянии приложения
- Часто дублируется с логикой сервисов мониторинга
- Должен быть крайним рубежом, а не основным методом
Стратегия 3: Адский промис: Расширенный контроль
Для сложных сценариев используйте декларативные обертки:
async function executeWithRetry(operation, { retries = 2 } = {}) {
let attempt = 0;
while (attempt <= retries) {
try {
return await operation();
} catch (error) {
attempt++;
if (!isRetryableError(error) || attempt > retries) {
throw error; // Выбрасывать для вышестоящих обработчиков
}
await delay(100 * Math.pow(2, attempt));
}
}
}
// Использование:
executeWithRetry(() => paymentService.charge(amount))
.catch(handlePaymentFailure);
Особенности реализации:
- Экспоненциальная отсрочка снижает нагрузку при сбое системы
isRetryableError
определает только повторяемы ошибки (сетевая забота, 5xx)- Возвращаем результат
operation()
, сохраняя семантику вызова
Параллельные операции: Найди неудачника в толпе
Promise.all
прервется при первой ошибке:
// Подход с Promise.allSettled для комплексного анализа
async function fetchDashboardData() {
const results = await Promise.allSettled([
fetchUser(),
fetchPermissions(),
fetchNotifications()
]);
const errors = results.filter(r => r.status === 'rejected');
if (errors.length > 0) {
errors.forEach(e =>
logger.warn('Partial data failure', e.reason));
}
return {
user: getResult(results[0]),
permissions: getResult(results[1]),
notifications: getResult(results[2])
};
}
// Вспомогательная функция для работы с неопределённым состоянием
function getResult(result) {
return result.status === 'fulfilled' ? result.value : null;
}
Решение для задач требовательных к завершению:
async function processBatch(batch) {
const promises = batch.map(order =>
processOrder(order).catch(e => {
recordFailedOrder(order.id, e);
return null; // Возвращаем отменяемую операцию
})
);
return (await Promise.all(promises)).filter(Boolean);
}
Архитектурные решения: Создание структуры ошибок
Стандартизация модели ошибок:
class DomainError extends Error {
constructor(message, { cause, code, retryable } = {}) {
super(message);
this.code = code ?? 'GENERIC_ERROR';
this.retryable = retryable ?? false;
if (cause) this.cause = cause;
}
}
class DatabaseError extends DomainError {
constructor(message, options) {
super(message, { ...options, code: 'DB_OPERATION_FAILED' });
}
}
// Применение:
try {
await db.query('...');
} catch (error) {
throw new DatabaseError('Failed to execute query', {
cause: error,
retryable: true
});
}
Преимущества:
- CATALOG_ERRORS для стандартизации ответов API
- Централизованный перехват в middleware Express/Koa/Fastify
- Возможность автоматической стратегии повтора
- Интеграция с системами мониторинга (Sentry, OpenTelemetry)
Заключение: Философия устойчивости
Обработка асинхронных ошибок — не рутинное примечание при программировании, а фундаментальная часть архитектуры:
- Никогда не доверяйте молчанию: Незахваченные отклонения сигнализируют о неисправности системы
- Контекст убивает неопределенность: Всегда привязывайте ошибки к бизнес-операциям
- Различайте последствия: Сбои сетей требуют повторных попыток; ошибки валидации требуют немедленного прерывания работы
- Проектируйте для отслеживания: Логируйте с применением структурных данных; используйте уникальные коды ошибок
Сломайте цепь безмолвных сбоев. Точная обработка асинхронных ошибок отличает хрупкий прототип от промышленного ПО. Ваши ошибки должны завершать работу с оглушительным грохотом, а не умирать шепотом.