JavaScript
Programming
July 14

Promise.allSettled

Original author: Матиас Байненс
Translation


На 71-м митинге Ecma TC39 будет рассматриваться проект и эталонная реализация Promise.allSettled — третьего из четырех основных комбинаторов промисов.


Авторы: Джейсон Вильямс (BBC), Роберт Памли (Bloomberg), Матиас Байненс (Google)
Чемпион: Матиас Байненс (Google)
Этап: 3


Для любителей подкастов, продублировано на YouTube.


Введение и мотивация


В мире промисов существует четыре основных комбинатора:


  • Promise.all. ES2015. Замыкается на первом отклоненном/rejected промисе.
  • Promise.race. ES2015. Замыкается на первом хоть как-то разрешенном/settled промисе.
  • Promise.any. Stage 1. Замыкается на первом удовлетворенном/fulfilled промисе.
  • Promise.allSettled. Stage 3 → Stage 4. Не замыкается.

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


Основное применение этого комбинатора наступает, когда хочется выполнить действие сразу после завершения множества запросов, вне зависимости, закончились ли они успехом или неудачей. Остальные комбинаторы промисов замыкаются (short-circuit), выбрасывая результаты входящих значений, проигравших в гонке за определённым состоянием системы. Promise.allSettled уникален тем, что всегда ожидает всех, за кого отвечает.


Promise.allSettled возвращает промис, который выполняется с возвращением массива снапшотов состояний промисов, но лишь только после того, как совершенно все исходные промисы разрешены (settled).


Откуда взялось название allSettled?


Мы говорим, что промис разрешен (settled), если он не подвис в ожидании (pending), т.е. когда он либо удовлетворён, либо отклонён — одно из двух. Чтобы разобраться в терминологии, взгляните на старый документ States and Fates.


А ещё, это имя, allSettled, широко используется в существующих библиотеках, реализующих данную функциональность. Список будет ниже.


Примеры


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


function reflect(promise) {
  return promise.then(
    (v) => {
      return { status: 'fulfilled', value: v };
    },
    (error) => {
      return { status: 'rejected', reason: error };
    }
  );
}

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.all(promises.map(reflect));
const successfulPromises = results.filter(p => p.status === 'fulfilled');

Предлагаемое API позволяет разработчику обработать эти варианты, без необходимости создавать функцию reflect самостоятельно, или заниматься хранением результатов во временных переменных. Новое API выглядит так:


const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
const successfulPromises = results.filter(p => p.status === 'fulfilled');

Если же нам почему-то нужны отклонённые промисы, то вероятно, нужно собрать причины произошедшего. allSettled позволяет сделать это так же просто.


const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];

const results = await Promise.allSettled(promises);
const errors = results
  .filter(p => p.status === 'rejected')
  .map(p => p.reason);

Реальные примеры


Довольно распространённым является желание знать, что все запросы выполнились, вне зависимости от состояния каждого из них. Это важно, когда хочется в будущем заняться постепенным улучшением. Не всегда нам нужно получить от API ответ.


const urls = [ /* ... */ ];
const requests = urls.map(x => fetch(x)); // Представьте, что-то из этого увенчается успехом, а что-то - нет.

// Вот этот комбинатор остановится на первом же отказе, а ответы потеряются.
try {
  await Promise.all(requests);
  console.log('Все запросы вернулись, можно убрать полоску загрузки.');
} catch {
  console.log('Какой-то из запросов явно отвалился, но другие могут продолжать работать. Ой.');
}

С использованием Promise.allSettled можно написать нечто, что больше соответствует нашим ожиданиям.


// Мы точно знаем, что все запросы к API уже отработали.
Promise.allSettled(requests).finally(() => {
  console.log('Все запросы завершены: успешно или с ошибкой, сейчас всё равно');
  removeLoadingIndicator();
});

Пользовательские реализации



В других языках


Похожая функциональность существует и в других языках, под другими именами. Поскольку не существует универсального механизма, совместимого сразу со множеством языков, этот документ следует прмеру наименований из библиотек, перечисленных выше. Вот как это выглядит в других языках:


  • Rust — futures::join;
  • C# — Task.WhenAll. Можно использовать либо try/catch, либо TaskContinuationOptions.OnlyOnFaulted;
  • Python — asyncio.wait с опцией ALL_COMPLETED
  • Java — CompletableFuture.allOf

Материалы для дальнейшего изучения



Минутки со встреч TC39



Спецификация



Реализации



+27
7k 41
Comments 37
Top of the day