javascript

Тихий Promise, который убьёт ваш сервер Node.js

  • суббота, 24 января 2026 г. в 00:00:12
https://habr.com/ru/articles/988132/

У тебя продакшн-сервер. Он спокойно работал часами.

А потом внезапно умер. Без предупреждения, без плавного деградирования. Просто мёртв.

Виновник? Одна-единственная строчка кода, которая выглядит абсолютно безобидно:

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 есть чёткие правила:

  1. try-catch ловит только синхронные throw и reject-ы промисов под await

  2. Промис без await, .catch() или .then(…, onRejected) — это бомба с таймером

  3. «Fire and forget» всё равно требует .catch(): ты забываешь результат, но не забываешь ошибку

Твой сервер скажет тебе спасибо.

PS муза пришла пока работал над чатИИком 😁

EDIT: статься отредактирована, спасибо @nikulin_krd за то что указал на ошибку в примерах 🙌