Промисы и async/await в JavaScript: понятное руководство с примерами и лучшими практиками

Промисы и async/await в JavaScript: понятное руководство с примерами и лучшими практиками

Промисы и async/await в JavaScript: понятное руководство с примерами и лучшими практиками

Промисы и async/await в JavaScript: понятное руководство с примерами и лучшими практиками

Запрос: «промисы в JavaScript для начинающих», «async await примеры». Это руководство поможет быстро освоить асинхронный код, избежать частых ошибок и писать надёжно и читаемо.

Что такое промис простыми словами

Promise — это объект, который представляет результат асинхронной операции: «ожидание» (pending), «успех» (fulfilled) или «ошибка» (rejected). Работать с ним можно через then/catch/finally или с помощью синтаксиса async/await.

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve(42), 500);
});

p
  .then(value => value * 2)
  .then(double => console.log('Результат:', double))
  .catch(err => console.error('Ошибка:', err))
  .finally(() => console.log('Готово'));

async/await: тот же промис, но читабельнее

async-функция возвращает промис. Оператор await «ждёт» промис и возвращает его значение или бросает исключение при отклонении.

async function run() {
  try {
    const value = await p; // 42 через ~500 мс
    console.log('Результат:', value * 2);
  } catch (e) {
    console.error('Ошибка:', e);
  } finally {
    console.log('Готово');
  }
}

run();

Последовательное vs параллельное выполнение

Частая ошибка — выполнять независимые задачи последовательно вместо параллельного запуска. Сравним:

// Эмулятор загрузки
const load = (id) => new Promise(res => setTimeout(() => res(`item-${id}`), 300));
const ids = [1, 2, 3, 4];

// Плохо: последовательно (дольше)
async function sequential() {
  const out = [];
  for (const id of ids) {
    out.push(await load(id));
  }
  return out;
}

// Хорошо: параллельно
async function parallel() {
  return Promise.all(ids.map(load));
}

sequential().then(r => console.timeEnd('seq'));
console.time('seq');
parallel().then(r => console.timeEnd('par'));
console.time('par');

Итог: используйте Promise.all для независимых задач. Последовательность нужна, когда следующий шаг зависит от результата предыдущего или вы ограничиваете конкуренцию.

Полезные комбинаторы: all, allSettled, race, any

// 1) Ждём всех, падаем если хоть один упадёт
await Promise.all([taskA(), taskB(), taskC()]);

// 2) Ждём всех, всегда получаем статусы
const results = await Promise.allSettled([taskA(), taskB()]);
// results: [{ status: 'fulfilled', value }, { status: 'rejected', reason }]

// 3) Кто быстрее — тот и результат (включая ошибку)
const first = await Promise.race([slowTask(), fastTask()]);

// 4) Ждём первый успешный, если все упали — AggregateError
try {
  const ok = await Promise.any([mayFail1(), mayFail2()]);
} catch (e) {
  // e instanceof AggregateError
}

Таймауты и отмена: практичные шаблоны

Таймаут через Promise.race:

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

// Пример:
await withTimeout(load(1), 500);

Отмена через AbortController (поддерживается fetch и многими API):

const controller = new AbortController();
const { signal } = controller;

setTimeout(() => controller.abort(), 2000); // отменим через 2с

try {
  const res = await fetch('https://example.com/data', { signal });
  const data = await res.json();
  console.log(data);
} catch (e) {
  if (e.name === 'AbortError') {
    console.warn('Запрос отменён');
  } else {
    console.error('Ошибка запроса', e);
  }
}

Типичные ошибки и как их избежать

  • Забыли вернуть промис в .then — цепочка «теряется». Решение: всегда return внутри then, если используете цепочки.
  • Смешивание then и await в одном фрагменте — ухудшает читаемость. Выберите один стиль на участок кода.
  • async в forEach. forEach не ждёт промисы — используйте for…of или map + Promise.all.
  • Нет обработки ошибок — получите unhandledrejection. Всегда ловите ошибки try/catch или .catch.
  • Пример с forEach: как правильно

    // Плохо: не ждёт завершения
    ids.forEach(async (id) => {
      await load(id);
    });
    
    // Хорошо 1: последовательная обработка
    for (const id of ids) {
      await load(id);
    }
    
    // Хорошо 2: параллельно и ждём всех
    await Promise.all(ids.map(id => load(id)));
    

    Преобразование колбэков в промисы (promisify)

    Иногда встречаются функции формата callback(err, result). Их удобно оборачивать в промисы.

    function toPromise(fn) {
      return (...args) => new Promise((resolve, reject) => {
        fn(...args, (err, result) => (err ? reject(err) : resolve(result)));
      });
    }
    
    // Пример с псевдо-колбэком
    function legacyMultiply(x, cb) {
      setTimeout(() => cb(null, x * 2), 200);
    }
    
    const multiplyAsync = toPromise(legacyMultiply);
    const value = await multiplyAsync(5); // 10
    

    Ограничение конкуренции (concurrency limit)

    Не всегда можно запускать сотни запросов параллельно — лимиты и ресурсы не бесконечны. Используйте паттерн mapLimit.

    async function mapLimit(items, limit, iteratee) {
      const results = [];
      const executing = new Set();
    
      for (const item of items) {
        const p = Promise.resolve()
          .then(() => iteratee(item))
          .then((res) => {
            results.push(res);
            executing.delete(p);
          });
    
        executing.add(p);
        if (executing.size >= limit) {
          await Promise.race(executing);
        }
      }
    
      await Promise.all(executing);
      return results;
    }
    
    // Пример использования
    const out = await mapLimit(ids, 2, load); // максимум 2 одновременные задачи
    

    Чеклист лучших практик

  • Независимые задачи запускайте через Promise.all — это быстрее.
  • Добавляйте таймауты к сетевым операциям и умейте отменять (AbortController).
  • Стандартизируйте стиль: либо then-цепочки, либо async/await на участке кода.
  • Ловите ошибки: try/catch и .catch; логируйте и показывайте понятные сообщения.
  • Следите за конкуренцией: используйте лимиты и очереди для тяжёлых задач.
  • Не используйте async в forEach/Map/Filter без понимания поведения — применяйте for…of или Promise.all.
  • Куда двигаться дальше

    Закрепить тему можно на реальных мини-проектах: параллельная загрузка ресурсов с лимитом, отмена долгих запросов, обработка частично успешных результатов через allSettled. Если хотите системно пройти основы и быстро перейти к уверенной практике, загляните сюда: Освоить асинхронность и не только на курсе «JavaScript с Нуля до Гуру 2.0» — стартуйте сегодня.

    Промисы и async/await — фундамент современного JavaScript. Освоив их, вы пишете менее «шумный» и более надёжный код, контролируете конкурентность и уверенно обрабатываете ошибки.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий