У тебя продакшн-сервер. Он спокойно работал часами.
А потом внезапно умер. Без предупреждения, без плавного деградирования. Просто мёртв.
Виновник? Одна-единственная строчка кода, которая выглядит абсолютно безобидно:
saveMessageToDatabase(data);Ситуация
Ты пишешь API для чата. Хочешь, чтобы ответ от ИИ сразу полетел пользователю стримингом, а сохранение в базу шло фоном. Классический fire-and-forget:
async function handleChat(request) {
try {
const stream = await callAI(request);
// Запустили и забыли — не ждём
saveMessageToDatabase(stream);
return stream;
} catch (error) {
console.error('Failed:', error);
}
}Выглядит нормально. Что может пойти не так?
Убийственный выстрел
Через три часа у провайдера ИИ случается сбой. Соединение со стримингом рвётся на полпути. saveMessageToDatabase кидает ошибку.
Сервер падает.
«Да ладно, PM2 / Docker / Kubernetes его перезапустит!»
Да — но ты уже потерял все запросы, которые были в полёте, весь несохранённый стейт, а пользователи увидели 500-ю ошибку.
Профилактика лучше, чем восстановление.
Почему try-catch тебя не спас
try-catch ловит ошибки только от ожидаемых (await) промисов:
// ❌ Этот try-catch бесполезен
try {
saveMessageToDatabase(stream); // сразу возвращает Promise
} catch (error) {
// этот код никогда не выполнится
}Без await функция возвращает промис и мгновенно выходит из try — всё ок. Настоящая ошибка происходит позже, асинхронно — когда try-catch уже давно закончился.
Когда этот промис в итоге reject-ится и никто его не обработал → Node.js видит unhandled rejection и убивает процесс фатальной ошибкой.
Исправление
Один символ. Ну, то есть, несколько:
saveMessageToDatabase(stream).catch(() => {});Всё.
Добавив .catch() ты говоришь Node: «Да, я знаю, что это может упасть, и я это осознанно обрабатываю».
Колбэк может быть пустым, если ты логируешь ошибку внутри функции. Главное пометить промис как «обработанный».
Правильный паттерн
Для любой операции fire-and-forget:
// Всегда вешай .catch() на «висящие» промисы
doSomethingAsync().catch(() => {});
// Или логируй, если хочешь видеть проблему
doSomethingAsync().catch(err => {
console.error('Фоновая задача упала:', err);
});Главный урок
У try-catch и async/await есть чёткие правила:
try-catch ловит только синхронные throw и reject-ы промисов под await
Промис без await, .catch() или .then(…, onRejected) — это бомба с таймером
«Fire and forget» всё равно требует .catch(): ты забываешь результат, но не забываешь ошибку
Твой сервер скажет тебе спасибо.
PS муза пришла пока работал над чатИИком 😁
EDIT: статься отредактирована, спасибо @nikulin_krd за то что указал на ошибку в примерах 🙌
