Promises in Javascript

In Javascript, a promise is an object that represents the completion or failure of an asynchronous task.

You can think of promises as returned objects to which you can attach a callback (a fancy term for a function that gets called-back ~hehe~) as opposed to passing the callback into the function.

In my opinion, the best way to learn code is to read code so let's go over a simple example.

Imagine you have a function generateJazzMusicAsync() which asynchronously generates jazz music. You want to pass a callback function that will play the jazz music once it is successfully generated or play rap music if the task failed to generate the jazz music.

In the olden days, you would pass a callback function

// imagine that generateJazzMusicAsync's implementation is provided elsewhere in this file
const preMadeRapMusic = generateRapMusic();

const succeededCallback = (input) => {
    playMusic(input);
};

const failedCallback = (error) => {
    playMusic(preMadeRapMusic);
};

generateJazzMusicAsync(succeededCallback, failedCallback);

But in the modern new world, we can use promises.

const preMadeRapMusic = generateRapMusic();

const succeededCallback = (input) => {
    playMusic(input);
};

const failedCallback = (error) => {
    playMusic(preMadeRapMusic);
};


generateJazzMusicAsync().then(succeededCallback).catch(failedCallback);

Some of the main advantageous of using promises include:

Guarantees

Unlike the old and tired passed-in callbacks, promises provides some garantees for us to base our logic on.

  • Callbacks added with then(), as above, will be called after the promise successfully resolves.
  • You can clearly specify how to handle errors in the catch() block which if chained with another .then() can provide you with a guarantee of how foreseen and unforeseen errors will be handled.
  • Multiple callbacks may be added by calling then()several times. Each callback is executed one after another, in the order in which they were inserted.

Resolution Control

Promises provide you with 4 functions to manage how you want your promises to be completed.

  • Promise.all() takes in an array of promises as an input and then resolves to an array of the results from each promise. The returned promise will only resolve when ALL of the input promises resolve successfully, Promise.all() will reject immediately if ANY promises were rejected and it will reject with the first error message occurred.

    • Used when: You need all the promises to complete together. You want to the process to stop as soon as any promise fails.

  • Promise.any() takes in an array of promises as an input and it will resolve as soon as ANY of the promises succeed/resolve successfully. It will resolve to a single promise, the one that succeeded first. If none of the promises are fulfilled, then it will reject with AggregateError.

    • Used when: You need any promise to succeed. You don't care if all of them succeed as long as a single one does.

  • Promise.race() takes in an array of promises as an input and it will resolve as soon as ANY of the promises succeed OR reject. You can think of it as racing to see which promise will finish first whether it succeeds or not. This promise will either reject or resolve to whatever the first promise to finish rejected or resolved to.

    • Used when: You need any promise to settle. You want any promise to either succeed or reject without regard to any of the other promises.

  • Promise.allSettled() takes in an array of promises as an input and it will resolve when ALL promises have settled, that is when all promises have rejected or resolved. It will return an array of statuses (full filled or rejected) as well as the accompanying error messages or resolved values.

    • Used when: You need all promises to settle, you want all promises to try and perform their tasks regardless of whether its sibling promise failed or succeeded. (Honestly i try to use this whenever possible).

Chains

In software, you will commonly want to execute tasks/functions back to back in a sequential fashion, to accomplish this with asynchronous tasks we will chain promises together.

doThing()
  .then((res1) => {
      return doSomethingElse();
    })
    .then((res2) => {
      return doAnotherThing();
    })
    .catch((error) => {
      console.error("Something went wrong while doing the thing");
    });

Gotcha: Always return results if you want the promise chain to make any sense. You need to return something from the first promise to act as input for the second promise.