Промисы в JavaScript: от основ до сложных решений

Раздел: JavaScript -> асинхронное программирование в JavaScript

Промисы в JavaScript: концепция и базовое использование

Как эффективно организовать асинхронный код с промисами?

Наиболее современный и читаемый способ работы с промисами - это использование синтаксиса async/await. Этот подход позволяет писать асинхронный код так, как если бы он был синхронным, что упрощает чтение и отладку.

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Ошибка при загрузке данных:', error);
  }
}

Async js (асинхронный javascript)

Функция async всегда возвращает промис. Ключевое слово await приостанавливает выполнение до разрешения промиса. Ошибки обрабатываются с помощью try/catch.

Типичная проблема: Забытый await внутри async-функции может привести к тому, что код продолжит выполнение до завершения промиса, что вызовет неожиданное поведение. Всегда используйте await для промисов внутри async.

Как использовать цепочки промисов без async/await?

Традиционный способ - методы .then() и .catch(). Промис - это объект, представляющий будущее значение. Метод .then() принимает два колбэка: onFulfilled и onRejected, но чаще используют отдельный .catch().

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('Ошибка:', error);
  });

Js promise (промисы в javascript)

Цепочки позволяют последовательно выполнять асинхронные операции, передавая результат от одного .then() к другому.

Распространенная ошибка: Не возвращать промис из колбэка .then(). Если внутри .then() не вернуть значение (или промис), следующий .then() получит undefined. Всегда возвращайте результат асинхронной операции.

Как дождаться выполнения нескольких независимых промисов?

Для параллельного выполнения используйте Promise.all(). Он принимает массив промисов и возвращает один промис, который разрешается массивом результатов, когда все промисы завершатся успешно. Если любой промис отклоняется, весь Promise.all отклоняется.

const urls = ['url1', 'url2', 'url3'];
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.all(promises);
console.log(results);

Вариант: Promise.allSettled() ожидает завершения всех промисов независимо от их статуса и возвращает массив объектов с полями status и value или reason.

const results = await Promise.allSettled(promises);
results.forEach(r => console.log(r.status, r.value || r.reason));

Если один из промисов, переданных в Promise.all, отклоняется, остальные результаты будут потеряны. Для устойчивости используйте Promise.allSettled или добавьте обработку ошибок для каждого промиса.

Как выполнить промис с таймаутом или выбрать первый успешный?

Для таймаута используйте Promise.race(). Он возвращает результат первого завершенного промиса (как успешного, так и отклоненного).

const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000));
const dataPromise = fetch('https://api.example.com/data').then(r => r.json());
const result = await Promise.race([dataPromise, timeout]);
console.log(result);

Чтобы получить первый успешный результат, используйте Promise.any(). Если все промисы отклонены, возвращается AggregateError.

const promises = [slowFetch(), fastFetch()];
const firstSuccess = await Promise.any(promises);
console.log(firstSuccess);

При использовании Promise.race() отклонение первого промиса может привести к тому, что остальные промисы продолжат выполнение, но их результаты будут проигнорированы. Это может быть неэффективно. Рассмотрите отмену лишних операций через AbortController.

Как обрабатывать ошибки в цепочках промисов?

В промисовых цепочках ошибка передается в ближайший .catch() или может быть обработана через второй аргумент .then(). Чаще всего используют единый .catch() в конце цепочки. Если нужно продолжить выполнение после ошибки, поместите .catch() после конкретного шага и верните значение по умолчанию.

fetch('/data')
  .then(r => r.json())
  .catch(err => {
    console.warn('Не удалось загрузить данные, используем запасные', err);
    return { default: true };
  })
  .then(data => console.log(data));

Если .catch() не возвращает промис или значение, следующий .then() получит undefined. Важно корректно возвращать значения из обработчиков.

Расширенные примеры и нестандартные сценарии

Ниже представлены более сложные примеры использования промисов, включая отмену запросов, последовательное выполнение с контролем потока и создание собственных промисов.

Пример 1: Отмена запроса с помощью AbortController и Promise.race

Пример
const controller = new AbortController();
const signal = controller.signal;

const fetchWithTimeout = (url, timeout = 5000) => {
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), timeout)
  );
  const fetchPromise = fetch(url, { signal }).then(r => r.json());
  return Promise.race([fetchPromise, timeoutPromise])
    .finally(() => controller.abort()); // отменяем запрос в любом случае
};

try {
  const data = await fetchWithTimeout('https://api.example.com/slow', 3000);
  console.log(data);
} catch (err) {
  console.error('Ошибка или таймаут:', err.message);
}
Ошибка или таймаут: Timeout

Объяснение: Используется AbortController для отмены fetch-запроса. Promise.race определяет, какое событие наступит раньше. .finally() вызывает отмену после завершения любого из промисов, чтобы не расходовать ресурсы.

Пример 2: Последовательное выполнение с ограничением параллелизма

Пример
async function asyncPool(concurrency, items, iteratorFn) {
  const executing = new Set();
  const results = [];
  for (const [index, item] of items.entries()) {
    const p = Promise.resolve().then(() => iteratorFn(item, index));
    results.push(p);
    executing.add(p);
    const clean = () => executing.delete(p);
    p.then(clean, clean);
    if (executing.size >= concurrency) {
      await Promise.race(executing);
    }
  }
  return Promise.all(results);
}

// Использование:
const urls = ['url1', 'url2', 'url3', 'url4'];
const fetchData = async (url) => {
  const res = await fetch(url);
  return res.json();
};
const data = await asyncPool(2, urls, fetchData);
console.log(data);
[массив результатов в порядке urls]

Объяснение: Функция asyncPool поддерживает пул одновременно выполняемых промисов. Когда количество текущих промисов достигает лимита, ожидается завершение хотя бы одного из них. После этого запускается следующий. Это помогает избежать перегрузки сети или сервера.

Пример 3: Создание промиса с кастомной логикой повтора (retry)

Пример
function fetchWithRetry(url, options = {}, retries = 3) {
  return new Promise((resolve, reject) => {
    const attempt = (n) => {
      fetch(url, options)
        .then(response => {
          if (!response.ok) throw new Error('HTTP error ' + response.status);
          return response.json();
        })
        .then(resolve)
        .catch(err => {
          if (n > 0) {
            console.log(`Попытка ${retries - n + 1} не удалась. Повтор...`);
            setTimeout(() => attempt(n - 1), 1000);
          } else {
            reject(err);
          }
        });
    };
    attempt(retries);
  });
}

// Использование:
fetchWithRetry('https://unstable.example.com', {}, 5)
  .then(data => console.log('Данные получены:', data))
  .catch(err => console.error('Не удалось загрузить после всех попыток:', err));
Попытка 1 не удалась. Повтор...
Попытка 2 не удалась. Повтор...
Данные получены: {...}

Этот паттерн полезен при работе с ненадежными сетевыми соединениями. setTimeout вводит задержку перед повторной попыткой, чтобы не перегружать сервер.

Пример 4: Комбинация Promise.allSettled и обработка частичных результатов

Пример
const promises = [
  fetch('/user/1').then(r => r.json()),
  fetch('/user/2').then(r => r.json()),
  fetch('/user/3').then(r => r.json())
];

const settled = await Promise.allSettled(promises);
const successful = settled.filter(r => r.status === 'fulfilled').map(r => r.value);
const failed = settled.filter(r => r.status === 'rejected').map(r => r.reason);

console.log('Успешные:', successful);
console.log('Ошибки:', failed);
Успешные: [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]
Ошибки: [ Error: 404 Not Found ]

Позволяет обработать частичный успех, не теряя данные из-за одного отказа.

Пример 5: Промис с возможностью отмены через внешний триггер

Пример
function cancellablePromise(executor) {
  let cancel;
  const promise = new Promise((resolve, reject) => {
    cancel = reject;
    executor(resolve, reject);
  });
  return { promise, cancel: () => cancel(new Error('Cancelled')) };
}

const { promise, cancel } = cancellablePromise((resolve) => {
  setTimeout(() => resolve('Готово'), 5000);
});

// Через 2 секунды отменим
setTimeout(() => cancel(), 2000);

promise
  .then(console.log)
  .catch(err => console.error('Промис отменён:', err.message));
Промис отменён: Cancelled

Такой подход может быть полезен при реализации таймеров или запросов, которые нужно прерывать по пользовательскому действию.

промисы в JavaScript - comments

En
Js promise (javascript)