07. Promise Composition

Существует две системных функции для комбинирования Promise:

  • Promise.all соответствует приему "для всех".

  • Promise.race соответствует принципу winner takes it all.

Promise.all

Метод Promise.all(array: Promise) принимает на вход массив Promise и возвращает новый Promise, который будет выполнен когда пока все входящие в него Promise будут выполнены.

  • Promise.all() и возвращает массив результатов.

  • Note that even if a single dependency is rejected, the Promise.all method will be rejected entirely as well.

  • If an empty iterable is passed, then this method returns (synchronously) an already resolved promise.

  • If all of the passed-in promises fulfill, or are not promises, the promise returned by Promise.all() is fulfilled asynchronously.

  • If any of the passed-in promises reject, Promise.all asynchronously rejects with the value of the promise that rejected, whether or not the other promises have resolved.

const p1 = Promise.resolve(3);
const p2 = 1337;
const p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "foo");
});

Promise.all([p1, p2, p3]).then(values => {
  console.log(values); // [3, 1337, "foo"]
});

Promise.all is good for executing many promises at once:

Promise.all([promise1, promise2]);

Promise.race

The Promise.race(iterable) method returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.

  • Результатом будет только первый settled Promise из списка (неважно как - успешно или неуспешно).

  • Успешным считается любой Promise, даже если он был rejected.

  • Остальные игнорируются.

  • If the iterable passed is empty, the promise returned will be forever pending.

// we are passing as argument an array of promises that are already resolved,
// to trigger Promise.race as soon as possible
var resolvedPromisesArray = [Promise.resolve(33), Promise.resolve(44)];

var p = Promise.race(resolvedPromisesArray);
// immediately logging the value of p
console.log(p);

// using setTimeout we can execute code after the stack is empty
setTimeout(function() {
  console.log("the stack is now empty");
  console.log(p);
});

// logs, in order:
// Promise { <state>: "pending" }
// the stack is now empty
// Promise { <state>: "fulfilled", <value>: 33 }

Promise.race is good for setting a timeout:

Promise.race([
  new Promise((resolve, reject) => {
    setTimeout(reject, 10000); // timeout after 10 secs
  }),
  doSomethingThatMayTakeAwhile()
]);

Promise.allSettled

Promise.allSettled returns a promise that’s fulfilled with an array of promise state snapshots, but only after all the original promises have settled; i.e. become either fulfilled or rejected.

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises).then((results) => console.log(results));

/**
 * [
 *   { status: "fulfilled", value: 3 }, 
 *   { status: "rejected", reason: "foo" }
 * ]
 */

Sequential promise execution

One solution is to run the Promises in series, or one after the other. Unfortunately there’s no simple analog to Promise.all in ES6 (why?), but Array.reduce can help us:

let itemIDs = [1, 2, 3, 4, 5];

itemIDs.reduce((promise, itemID) => {
  return promise.then(_ => api.deleteItem(itemID));
}, Promise.resolve());

Такое использование reduce гарантирует, что then будут вызываться последовательно, один за другим, формируя последовательное исполнение. Можно использовать forEach, но это будет случайное исполнение:

itemIDs.forEach(itemID => {
  api.deleteItem(itemID);
});

Serial queue of events

It is possible to imitate serial queue of events using promises:

let queue = Promise.resolve();
function serialQueue(nexTask) {
  queue = queue
    .catch(() => {}) // to catch error from previous task
    .then(nextTask);
  return queue;
}

Dynamic chains

Sometimes we want to construct our Promise chain dynamically, e.g. inserting an extra step if a particular condition is met. Be sure to update the value of promise by writing promise = promise.then(/*...*/).

function readFileAndMaybeLock(filename, createLockFile) {
  let promise = Promise.resolve();

  if (createLockFile) {
    promise = promise.then(_ => writeFilePromise(filename + ".lock", ""));
  }

  return promise.then(_ => readFilePromise(filename));
}

Passing data between Promise callbacks

The following code illustrates a common problem with Promise callbacks: The variable connection (line A) exists in one scope, but needs to be accessed in other scopes (line B, line C).

db.open()
.then(connection => { // (A)
    return connection.select({ name: 'Jane' });
})
.then(result => {
    // Process result
    // Use `connection` to make more queries (B)
})
···
.catch(error => {
    // handle errors
})
.finally(() => {
    connection.close(); // (C)
});

We can solve this problem with Promises if we nest Promise chains:

db.open() // (A)
.then(connection => { // (B)
    return connection.select({ name: 'Jane' }) // (C)
        .then(result => {
            // Process result
            // Use `connection` to make more queries
        })
        ···
        .catch(error => {
            // handle errors
        })
        .finally(() => {
            connection.close();
        });
})

There are two Promise chains:

  • The first Promise chain starts in line A. connection is the asynchronously delivered result of open().

  • The second Promise chain is nested inside the .then() callback starting in line B. It starts in line C. Note the return in line C, which ensures that both chains are eventually merged correctly.

The nesting gives all callbacks in the second chain access to connection.

Another solutions for the same problem:

  • multiple return values

  • local variable and side effects (worst scenario)

  • async/await

const connection = await db.open();
try {
    const result = await connection.select({ name: 'Jane' });
    ···
} catch (error) {
    // handle errors
} finally {
    connection.close();
}

Last updated