Каждый разработчик, работающий с асинхронным кодом в JavaScript, рано или поздно сталкивается с ошибками, которые «проглатываются» без логов, неожиданными падениями приложения или неконтролируемыми состояниями в цепочках промисов. Эти проблемы особенно коварны в продакшен-среде, где их сложно воспроизвести и отладить. Разберемся, как построить надежную систему обработки ошибок — от базовых паттернов до архитектурных решений.
Почему ошибки «исчезают»?
Рассмотрим типичный сценарий с fetch
:
fetch('/api/data')
.then(response => response.json())
.then(data => updateUI(data));
Если сервер вернет 500 ошибку или JSON окажется некорректным, исключение может остаться неперехваченным. В браузере это приведет к ошибке в консоли, но в Node.js процесс вообще завершится с кодом 1. Решение кажется очевидным — добавить .catch()
, но на практике разработчики часто забывают обрабатывать ошибки в цепочках промисов или используют async/await
без try/catch
.
Ловушка Partial Error Handling
Даже при наличии обработчиков часто встречается неполное покрытие:
async function fetchData() {
try {
const res = await fetch('/api');
const data = await res.json();
return processData(data);
} catch (error) {
console.error('Fetch failed:', error);
}
}
Здесь не обрабатываются:
- Ошибки в
processData()
- Случаи, когда
res.ok === false
- Прерывание запроса через
AbortController
Стратегии полного покрытия
1. Обертка для HTTP-ответов
Создайте утилиту для проверки статуса:
async function safeFetch(url, options) {
const res = await fetch(url, options);
if (!res.ok) {
const error = new Error(`HTTP ${res.status}`);
error.response = res;
throw error;
}
return res;
}
Теперь любой ответ с кодом ≥400 будет вызывать исключение.
2. Комбинирование Promise.catch и async/await
Для сложных цепочек используйте гибридный подход:
async function loadData() {
const rawData = await fetch('/api')
.then(handleInterceptors)
.catch(error => {
throw new DataLoadError(error.message, { cause: error });
});
try {
return validateData(rawData);
} catch (validationError) {
throw new DataLoadError('Invalid format', {
cause: validationError
});
}
}
Кастомные классы ошибок сохраняют контекст:
class DataLoadError extends Error {
constructor(message, { cause }) {
super(message);
this.name = 'DataLoadError';
this.cause = cause;
}
}
3. Глобальные обработчики
Для критичных приложений добавьте страхующие механизмы:
// Браузер
window.addEventListener('unhandledrejection', e => {
logToService(e.reason);
e.preventDefault();
});
// Node.js
process.on('unhandledRejection', (reason, promise) => {
logToService(reason);
process.exit(1);
});
Но не заменяйте ими локальные обработчики — это крайняя мера.
Интеграция с мониторингом
Логирование ошибок в консоль бесполезно для продакшена. Интегрируйте:
- Sentry/Bugsnag для фронтенда
- OpenTelemetry + Grafana Loki для бэкенда
- Трейсинг ошибок через X-Request-Id
Пример для Node.js с Express:
app.use(async (err, req, res, next) => {
const errorId = uuidv4();
await sendToSentry({
id: errorId,
error: err,
context: {
route: req.path,
params: req.params,
user: req.user?.id
}
});
res.status(500).json({
error: 'Internal Error',
reference: errorId
});
});
Паттерны для сложных систем
Транзакционные операции
Для последовательности асинхронных вызовов используйте компенсирующие действия:
async function placeOrder(userId, items) {
const steps = [];
try {
const payment = await createPayment(userId, items);
steps.push(() => refundPayment(payment.id));
const inventoryReservation = await reserveInventory(items);
steps.push(() => releaseInventory(inventoryReservation.id));
await notifyShippingService(items);
return { success: true };
} catch (error) {
for (const compensate of steps.reverse()) {
await compensate();
}
throw error;
}
}
Отказоустойчивые цепочки
Для ненадежных сторонних API реализуйте retry с экспоненциальным откатом:
async function resilientFetch(url, options, retries = 3) {
try {
return await fetch(url, options);
} catch (error) {
if (retries <= 0) throw error;
const delay = 100 * Math.pow(2, 4 - retries);
await new Promise(r => setTimeout(r, delay));
return resilientFetch(url, options, retries - 1);
}
}
Грамотная обработка ошибок — не мера предосторожности, а обязательная часть проектирования системы. Она требует:
- Единой стратегии для клиента и сервера
- Детального контекста в ошибках
- Интеграции с мониторингом
- Регламента обработки в стиле кода
Начните с введения кастомных классов ошибок, добавьте автоматическую передачу контекста и внедрите компенсационные транзакции для критических операций. Помните: хорошая система обработки ошибок оплачивается часами отладки, которых вам удалось избежать.