Five things about Promise API that you may not know

ES2015 introduces Promise API as a replacement for callbacks. There are a few things that you may not know.

Promise and the Event Loop

Consider the following async code.

setTimeout(someFunc, 1000);

setTimeout function waits for one second and then places the function someFunc at the end of the event loop. All other functions in event loop are processed before someFunc is taken up for processing. In that way, the delay specified in setTimeout is the minimum delay.

Now, consider the following async code. (CodePen)

setTimeout(() => console.log(2), 0);
const p = Promise.resolve(1);
p.then(val => {
  console.log(val);
});

Since the delay in setTimeout is 0, we may expect 2 to be printed first. But unexpectedly, the fulfilment function is called first. How does it work?

ES2015 places the fulfilment function to be executed after the current tick. Every message (or tick) in the event loop has an associated queue. If there are any messages in that queue, JavaScript executes those as well. On the contrary, setTimeout places the function at the end of the event loop. That explains why the console shows 1 ahead of 2.

Resolves to a single object

You probably know it. But something that is easy to forget. The fulfilment function has only one argument.

const p = new Promise((resolve, reject) => {
  resolve(911, 100);
});
p.then((val1, val2) => {
  console.log(val1, val2); // 911, undefined
});

To send more than one value, pack the values in an array or an object.

const p = new Promise((resolve, reject) => {
  resolve([911, 100]);
});
p.then(([val1, val2]) => {
  console.log(val1, val2);
});

We use array de-structuring to get more than one argument. The rejection function also accepts only one argument.

Unwraps a thenable recursively

We are familiar with the below code.

const p2 = Promise.resolve(p);

If p is some value, then the above code creates a new promise whose fulfilment function receives p.

If p is a ES6 Promise, it returns that. (p === p2)

But what if p is a thenable (any object with then function defined)? In that case, it is unwrapped recursively.

const someObject = {
  then(cbOuter) {
    cbOuter({
      then(cbInner) {
        cbInner('theSecret');
      }
    });
  }
};

const p = Promise.resolve(someObject);
p.then(val => {
  console.log(val); // theSecret
});

Play around with the CodePen to understand how it works.

Catch block for unhandled rejections

We provide a fulfilment function and a rejection function to every call of then method.

p.then(fulfilFn, rejectFn);

Normally, we omit supplying a reject handler. If no function is specified, JavaScript supplies a default function. The default handler bubbles up the resolved value or rejection error. This enables us to attach a final catch block as follows.

const p = Promise.reject('error');
p.then(null, null)
.catch(val => {
  console.log(val); // error
});

However, the catch block does not execute if we provide a rejection handler.

p.then(null, val => {
  console.log('I am handling the error here!');
})
.catch(val => {
  console.log(val);
});

There is no cancel or rollback

There is a race function.

Promise.race([
  Promise.resolve('I am first'),
  new Promise(resolve => {
    console.log('I am always printed!');
    resolve('I am second');
  })
]).then(val => {
  console.log(val);
});

As expected, JavaScript prints I am first. But JavaScript also resolves the second promise. It does not abort or cancel it. As a result, it prints I am always printed!. Do not use race function with alternatives that have side-effects. There is no cancel or rollback available.

Related Posts

Leave a Reply

Your email address will not be published.