Асинхронный код на JavaScript – фундамент современных веб-приложений. Однако элегантность async/await
часто оборачивается наивной обёрткой в try/catch
, прячущей критические проблемы. Рассмотрим практические стратегии обработки сбоев там, где стандартные подходы бьют мимо цели.
Почему try/catch
недостаточно
Попробуем получить данные через API:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
} catch (error) {
console.error('Ошибка загрузки:', error);
}
}
Что не так с этим подходом?
- HTTP-ошибки не ловятся:
fetch
не считает ответы 4xx/5xx ошибкой (reject), они проходят в успешный блок. - Потеря контекста: Один
catch
на все возможные ошибки усложняет диагностику. - Молчаливое проглатывание ошибки:
console.error
бесполезен в production.
Обрабатываем реальные ошибки HTTP
Доработаем функцию:
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
// Обрабатываем нестатусы HTTP программно
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
// Ошибка десериализации - не становится process exception
return await response.json();
}
Это лучше, но если response.json()
упадёт из-за битого JSON, ошибка выскочит наружу без указания источника данных – диагностика усложняется.
Работа с параллельными запросами
Используем Promise.all
:
async function fetchDashboardData() {
const [posts, comments] = await Promise.all([
fetchPosts(),
fetchComments()
]);
// ...
}
Бомба замедленного действия: любой из запросов рухнет – падает вся операция. Решение:
const results = await Promise.allSettled([
fetchPosts(),
fetchComments()
]);
const postsResult = results[0];
const commentsResult = results[1];
// Детальная обработка статуса операции:
if (postsResult.status === 'rejected') {
logError('Posts:', postsResult.reason, { severity: 'HIGH' });
return cachedPosts();
}
Интеграция с обработчиками глобальных событий
Дополните локальную обработку глобальным мониторингом:
// Браузер:
window.addEventListener('unhandledrejection', event => {
sendToLoggingService(event.reason);
});
// Node.js:
process.on('unhandledRejection', (reason, promise) => {
logClusterError(reason);
});
Но не глушите глобальные обработчики! Они – последний рубеж для критичных падений.
Абстракция обработчика ошибок
Убираем дублирование кода при помощи высокоуровневой обёртки:
const errorWrapper = handleErrors(async (fn, ...args) => {
await fn(...args);
});
const safeFetch = errorWrapper(async (url) => {
const res = await fetch(url);
if (!res.ok) throw new HttpError(res.status);
return res.json();
});
// Используем с контекстом
const user = await safeFetch('/api/users/1', {
meta: { component: 'UserView' }
});
Расширяем handleErrors
для интеграции аналитики и контекста:
export const handleErrors = (fn) => async (...args) => {
try {
return await fn(...args);
} catch (error) {
// Прикрепляем мета-данные к ошибке
error.meta = args.meta || {};
errorLogger.capture(error);
// Возвращаем fallback-результат или сбрасываем отображаемую ошибку
return getFallbackDataFor(error);
}
};
Ошибки времени ожидания
Простой таймаут для медленных соединений:
async function fetchWithTimeout(resource, timeout = 3000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {
signal: controller.signal
});
clearTimeout(id);
// Оставшаяся логика ...
}
Обратите внимание: отмена через AbortController
передаёт в Promise rejection с AbortError
.
Интеграция в архитектурные паттерны
Подключение middleware в Express/Koa:
// Express middleware ошибок
app.use(async (err, req, res, next) => {
const userMessage = customMessageMap[err.code] || 'Ошибка системы';
if (err.isOperational) {
res.status(err.status).json({ error: userMessage });
} else {
res.status(500).end();
await reportCriticalFailure(err);
}
});
В React-компоненте с использованием Error Boundaries:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack);
}
render() {
if (this.state.hasError) return <FallbackUI />;
return this.props.children;
}
}
Эволюция ошибок в production
Используйете принципы:
- Классификация: Разделение ошибок на операционные (ожидаемые) и программные (баги).
- Обогащение контекстом: Автоматическое добавление user ID, сессии, метаданных запроса.
- Ступенчатая отчетность: Разные ошибки – разные уровни алертинга.
Пример работы с операционными ошибками:
class DatabaseError extends Error {
constructor(details) {
super('Ошибка базы данных');
this.isOperational = true;
this.code = 'DB_CONNECT_FAIL';
this.details = details; // Технические детали для логов
}
}
// Использование
throw new DatabaseError({
query: params.query,
host: config.dbHost
});
Шепчущий код ошибки
Глубоко проработанная обработка ошибок меняет само использование асинхронных вызовов:
const { error, data } = await safeLoadData('/items');
if (error) {
showToast(`Не удалось: ${error.userMessage}`);
return handlePartialFailure(data);
}
processItems(data);
Преимущества:
- Конвейерная обработка ошибок и данных
- Чистая семантика для критического и ожидаемого сбоя
- Готовые данные для деградированного режима при гибридном повдении
Итоговый план мониторинга
Рабочий цикл обработки ошибок в продакшене включает:
- Дебаг-проваливание: точное определение места сбоя с помощью stack trace.
- Агрегация: объединение одинаковых ошибок в инциденты.
- Триггеры: автоматические оповещения при превышении порогов.
- Сброс состояния: автоматические откаты при обнаружении непреодолимого сбоя.
Ошибки – это не дыры в системе, а каналы обратной связи от реальности. Асинхронная обработка сбоев требует не обёртки блоком try/catch
, а проектирования отказоустойчивых интерфейсов взаимодействия с ненадёжным окружением. В этом ключ к стабильности приложения.