Представьте: ваше приложение падает в продакшене с сообщением UnhandledPromiseRejection
, но в локальной среде всё работает идеально. В логин-формах иногда «зависают» кнопки, а после навигации в React-приложении возникают необъяснимые сетевые запросы. Всё это – последствия некорректной обработки асинхронных операций. Разберём, почему стандартные try/catch
бессильны против плавающих багов и как создать надёжную систему обработки ошибок.
Почему промисы протекают
Типичная ошибка в Express-роутере:
app.post('/api/data', async (req, res) => {
const data = await fetchExternalAPI(); // Может упасть с исключением
res.json(data);
});
При сбое fetchExternalAPI
клиент никогда не получит ответа – обработчик Express замрёт. Решение выглядит очевидным:
app.post('/api/data', async (req, res) => {
try {
const data = await fetchExternalAPI();
res.json(data);
} catch (error) {
res.status(500).send('Error');
}
});
Но настоящие проблемы начинаются, когда мы комбинируем асинхронные операции. Пример из React-компонента:
useEffect(() => {
fetch('/data')
.then(response => response.json())
.then(data => setState(data));
}, []);
Если компонент размонтируется до завершения запроса, попытка обновить setState
вызовет ошибку. Браузер проигнорирует её, но утечка памяти останется.
Паттерны атомарной отмены
Рецепт – AbortController
для управления жизненным циклом операций:
useEffect(() => {
const controller = new AbortController();
const loadData = async () => {
try {
const response = await fetch('/data', {
signal: controller.signal
});
const data = await response.json();
if (!controller.signal.aborted) {
setState(data);
}
} catch (error) {
if (error.name !== 'AbortError') {
logError(error);
}
}
};
loadData();
return () => controller.abort();
}, []);
Ключевые моменты:
- Проверка
aborted
перед обновлением состояния - Фильтрация
AbortError
в catch - Интеграция с
fetch
, axios и другими библиотеками
Контроль над параллелизмом
Promise.all
прерывается при первой ошибке – не всегда желаемое поведение. Альтернатива:
const [users, posts] = await Promise.all([
fetch('/users').then(p => p.json()),
fetch('/posts').then(p => p.json())
]);
В реальности лучше использовать Promise.allSettled
с пост-обработкой:
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts()
]);
const errors = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (errors.length > 0) {
await sendErrorReport(errors);
}
const successfulData = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
Интеграция с системами мониторинга
Даже идеальная обработка ошибок не гарантирует их видимости. Настройте глобальные обработчики:
// Node.js
process.on('unhandledRejection', (reason, promise) => {
Sentry.captureException(reason);
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
// Браузер
window.addEventListener('unhandledrejection', event => {
event.preventDefault();
Sentry.captureException(event.reason);
});
Но помните – глобальный обработчик должен быть крайней линией защиты, а не заменой локальным try/catch
.
Когда async/await не панацея
Синтаксис async/await создаёт иллюзию синхронного кода, но скрывает важные нюансы. Пример опасного кода:
async function processBatch() {
const data = await loadData();
await Promise.all(data.map(async item => {
const details = await loadDetails(item.id); // Параллелизм?
await saveToDB(details); // Последовательные записи!
}));
}
Фикс через явное разделение задач:
async function processBatch() {
const data = await loadData();
const detailPromises = data.map(item =>
loadDetails(item.id)
.then(details => saveToDB(details))
);
await Promise.all(detailPromises);
}
Заключение
Основные принципы обработки асинхронных ошибок:
- Все асинхронные операции должны иметь явный механизм отмены
- Глобальные обработчики – для мониторинга, не для логики приложения
- Тестируйте сценарии с искусственными задержками и принудительными прерываниям
- Бенчмаркируйте память при частых асинхронных операциях
Чтобы полностью контролировать асинхронный код, перестаньте доверять ему. Каждое await
– потенциальная точка разрыва потока выполнения. Относитесь к цепочкам промисов как к системным ресурсам: явно создавайте, отслеживайте и обязательно освобождайте.