Ошибки в асинхронном JavaScript коде – не исключения, а неизбежность. Каждый вызов API, операция с базой данных, или файловая операция несет в себе потенциал сбоя. Как фронтенд, так и бэкенд разработчики сталкиваются с парадоксом: асинхронные операции критически важны, но их ошибки теряют контекст, проваливаются в пустоту, или падают приложение целиком. Рассмотрим системные подходы к обработке асинхронных ошибок за пределами базового try/catch
.
Минусы наивного подхода
Главная проблема ошибок в промисах и async/await – потеря стека вызовов. Рассмотрим типичный антипаттерн:
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json(); // Ошибка сети игнорируется!
}
Здесь любая сетевая ошибка выдаст необработанное исключение, обрушив процесс Node.js или "ломая" фронтенд. Стандартное решение:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
return response.json();
} catch (err) {
console.error('Fetch failed:', err); // Логирование – это только начало
}
}
Однако остаются нерешенными вопросы:
- Как передавать ошибку выше по стеку?
- Где обрабатывать бизнес-логику восстановления?
- Как избежать дублирования кода обработки?
Стратегия 1: Комбинированные промисы вместо try/catch
Для минимизации бойлерплейта используйте комбинацию .then()
и .catch()
с прокидыванием значений и ошибок:
function asyncHandler(promise) {
return promise
.then(data => [null, data])
.catch(err => [err, null]);
}
// Вместо try/catch:
const [userError, userData] = await asyncHandler(fetchUser(userId));
const [orderError, orderData] = await asyncHandler(fetchOrders(userId));
if (userError || orderError) {
// Единая точка обработки
}
Этот паттерн идеален для параллельных запросов с агрегированием ошибок.
Стратегия 2: Централизованная обработка через EventEmitter
На бэкенде (Node.js) создайте механизм централизованного уведомления об ошибках:
// errorHandler.js
const EventEmitter = require('events');
class ErrorTracker extends EventEmitter {}
const errorTracker = new ErrorTracker();
errorTracker.on('unhandled', (err, context) => {
logger.serviceError(err, { context });
metrics.increment('async_error');
if (err.critical) process.exit(1);
});
module.exports = errorTracker;
// В коде сервиса
async function refreshCache() {
try {
await cache.refresh();
} catch (err) {
errorTracker.emit('unhandled', err, { module: 'cache' }); // Перехват контекста
}
}
Преимущества:
- Логирование, нотификации и метрики в одном месте
- Возможность добавить контекст в момент возникновения
- Разделение бизнес-логики и инфраструктуры
Стратегия 3: Кастомные ошибки с трейсами и контекстом
Генерируйте ошибки со структурными метаданными для автоматизированного анализа:
class DatabaseError extends Error {
constructor(message, { query, params, code }) {
super(message);
this.name = 'DatabaseError';
this.details = { query, params, code };
Error.captureStackTrace(this, DatabaseError);
}
}
// В DAL-слое
async function queryDB(sql, params) {
try {
return await pool.query(sql, params);
} catch (dbErr) {
throw new DatabaseError('DB query failed', {
query: sql,
params,
code: dbErr.code
});
}
}
На фронтенде эти ошибки можно сериализовать для devtools, на бэкенде – агрегировать в PRM-системах.
Стратегия 4: Глобальные обработчики как страховка
Регистрируйте крайнюю линию защиты:
// Node.js
process.on('unhandledRejection', (reason) => {
errorTracker.emit('unhandled', reason);
});
// Браузер
window.addEventListener('unhandledrejection', (event) => {
telemetry.send('UNHANDLED_REJECTION', event.reason);
});
Важно: это должен быть не основной механизм, а система оповещения о пропущенных кейсах.
Выводы: Правила для production-кода
-
Контролируйте цепные реакции
Всегда выполняйте.catch()
для промисов без await
ИспользуйтеPromise.allSettled()
для массовых операций -
Добавляйте семантику
Преобразовывайте низкоуровневые ошибки в доменные
Обогащайте контекстом (id задачи, юзера, параметры запроса) -
Разделяйте управление ошибками
Обработчики на уровне приложения – для протоколирования
ПО промежуточного слоя – для HTTP-ответов
Доменные функции – для синтаксических проверок -
Автоматизируйте реакцию
Интегрируйте с системами мониторинга (Sentry, DataDog)
Настройте алерты по коду ошибки и частоте возникновения
Обработка асинхронных ошибок – проектирование failure paths с таким же вниманием, как и happy paths. Не используйте try/catch
как костыль для игнорирования проблем. Формализованная стратегия для ошибок обеспечивает предсказуемое поведение системы при частичных сбоях и ускоряет локализацию инцидентов.