Как дождаться выполнения Promise в Node.js?

«Как дождаться выполнения Promise в Node.js?» — вопрос из категории Асинхронность, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В Node.js я использую несколько подходов для работы с Promise в зависимости от контекста:

1. Async/await (рекомендуемый подход)

async function getUserData(userId) {
  try {
    // Ждем выполнение промиса
    const user = await User.findById(userId);

    // Параллельное выполнение независимых промисов
    const [orders, settings, logs] = await Promise.all([
      Order.find({ userId }),
      UserSettings.findOne({ userId }),
      AccessLog.find({ userId }).limit(10)
    ]);

    return { user, orders, settings, logs };
  } catch (error) {
    console.error('Failed to fetch user data:', error);
    throw new Error(`User data unavailable: ${error.message}`);
  }
}

2. Promise chaining для последовательных операций

function processUpload(filePath) {
  return validateFile(filePath)
    .then(metadata => resizeImage(metadata))
    .then(resized => uploadToS3(resized))
    .then(s3Url => saveToDatabase(s3Url))
    .then(result => {
      console.log('Upload completed:', result.id);
      return result;
    })
    .catch(error => {
      console.error('Upload pipeline failed:', error);
      cleanupTempFiles(filePath);
      throw error;
    });
}

3. Ожидание нескольких промисов с разными стратегиями

async function fetchWithFallbacks(urls) {
  // Promise.race - первый успешный или ошибочный
  const fastest = await Promise.race(urls.map(url => fetch(url)));

  // Promise.allSettled - все, независимо от результата
  const allResults = await Promise.allSettled(
    urls.map(url => fetch(url).then(r => r.json()))
  );

  const successful = allResults
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);

  return { fastest: await fastest.json(), successful };
}

4. Ожидание в не-async контекстах

// В обработчике события или callback
server.on('request', (req, res) => {
  // Используем IIFE (Immediately Invoked Function Expression)
  (async () => {
    try {
      const data = await processRequest(req);
      res.writeHead(200);
      res.end(JSON.stringify(data));
    } catch (error) {
      res.writeHead(500);
      res.end('Internal Server Error');
    }
  })();
});

// Или с .then() в цепочке
readFilePromise('config.json')
  .then(config => startServer(config))
  .then(server => {
    console.log('Server started on port', server.port);
    return server;
  })
  .catch(error => {
    console.error('Failed to start server:', error);
    process.exit(1);
  });

5. Top-level await (Node.js 14.8+)

// В ES-модулях можно использовать await на верхнем уровне
import { connectToDatabase } from './db.js';

const db = await connectToDatabase();
console.log('Database connected');

export { db };

Из моего опыта:

  • Всегда обрабатываю ошибки через try/catch с async/await
  • Для параллельных независимых операций использую Promise.all()
  • Для операций, где нужен первый результат, использую Promise.race() с timeout
  • В legacy-коде с callback иногда использую util.promisify()
  • Избегаю смешивания async/await и .then() в одной функции для читаемости