In JavaScript ES6, how can a thenable accept resolve and reject?

1.3k Views Asked by At

I think one principle I take so far is:

A promise is a thenable object, and so it takes the message then, or in other words, some code can invoke the then method on this object, which is part of the interface, with a fulfillment handler, which is "the next step to take", and a rejection handler, which is "the next step to take if it didn't work out." It is usually good to return a new promise in the fulfillment handler, so that other code can "chain" on it, which is saying, "I will also tell you the next step of action, and the next step of action if you fail, so call one of them when you are done."

However, on a JavaScript.info Promise blog page, it says the fulfillment handler can return any "thenable" object (that means a promise-like object), but this thenable object's interface is

.then(resolve, reject) 

which is different from the usual code, because if a fulfillment handler returns a new promise, this thenable object has the interface

.then(fulfillmentHandler, rejectionHandler)

So the code on that page actually gets a resolve and call resolve(someValue). If fulfillmentHandler is not just another name for resolve, then why is this thenable different?

The thenable code on that page:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // resolve with this.num*2 after the 1 second
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // shows 2 after 1000ms

3

There are 3 best solutions below

6
nonopolarity On

After writing down the whole explanation, the short answer is: it is because the JS promise system passed in a resolve and reject as the fulfillmentHandler and rejectionHandler. The desired fulfillmentHandler in this case is a resolve.

When we have the code

new Promise(function(resolve, reject) { 
  // ...
}).then(function() {
  return new Promise(function(resolve, reject) { 
    // ...
  });
}).then(function() { ...

We can write the same logic, using

let p1 = new Promise(function(resolve, reject) { 
  // ...
});

let p2 = p1.then(function() {
  let pLittle = new Promise(function(resolve, reject) { 
    // ...
    resolve(vLittle);
  });
  return pLittle;
});

The act of returning pLittle means: I am returning a promise, a thenable, pLittle. Now as soon as the receiver gets this pLittle, make sure that when I resolve pLittle with value vLittle, your goal is to immediately resolve p2 also with vLittle, so the chainable action can go on.

How does it do it?

It probably has some code like:

pLittle.then(function(vLittle) {         // ** Our goal **
  // somehow the code can get p2Resolve
  p2Resolve(vLittle);
});

The code above says: when pLittle is resolved with vLittle, the next action is to resolve p2 with the same value.

So somehow the system can get p2Resolve, but inside the system or "blackbox", the function above

            function(vLittle) {
  // somehow the code can get p2Resolve
  p2Resolve(vLittle);
}

is probably p2Resolve (this is mainly a guess, as it explains why everything works). So the system does

pLittle.then(p2Resolve);

Remember that

pLittle.then(fn) 

means

passing the resolved value of pLittle to fn and invoke fn, so

pLittle.then(p2Resolve);

is the same as

pLittle.then(function(vLittle) {
  p2Resolve(vLittle)
});

which is exactly the same as ** Our goal** above.

What it means is, the system passes in a "resolve", as a fulfillment handler. So at this exact moment, the fulfillment handler and the resolve is the same thing.

Note that in the Thenable code in the original question, it does

return new Thenable(result);

this Thenable is not a promise, and you can't resolve it, but since it is not a promise object, that means that promise (like p2) is immediately resolved as a rule of what is being returned, and that's why the then(p2Resolve) is immediately called.

So I think this count on the fact that, the internal implementation of ES6 Promise passes the p2Resolve into then() as the first argument, and that's why we can implement any thenable that takes the first argument resolve and just invoke resolve(v).

I think the ES6 specification a lot of time writes out the exact implementation, so we may somehow work with that. If any JavaScript engine works slightly differently, then the results can change. I think in the old days, we were told that we are not supposed to know what happens inside the blackbox and should not count on how it work -- we should only know the interface. So it is still best not to return a thenable that has the interface then(resolve, reject), but to return a new and authentic promise object that uses the interface then(fulfillmentHandler, rejectionHandler).

4
traktor On

In

let p2 = p1.then( onfulfilled, onrejected)

where p1 is a Promise object, the then call on p1 returns a promise p2 and records 4 values in lists held internally by p1:

  1. the value of onfulfilled,
  2. the value of the resolve function passed to the executor when creating p2 - let's call it resolve2,
  3. the value of onrejected,
  4. the value of the reject function passed to the executor when creating p2 - let's call it reject2.

1. and 3. have default values such that if omitted they pass fulfilled values of p1 on to p2, or reject p2 with the rejection reason of p1 respectively.

2. or 4. (the resolve and reject functions for p2) are held internally and are not accessible from user JavaScript.

Now let's assume that p1 is (or has been) fullfilled by calling the resolve function passed to its executor with a non thenable value. Native code will now search for existing onfulfilled handlers of p1, or process new ones added:

  • Each onfulfilled handler (1 above) is executed from native code inside a try/catch block and its return value monitored. If the return value, call it v1, is non-thenable, resolve for p2 is called with v1 as argument and processing continues down the chain.

  • If the onfulfilled handler throws, p2 is rejected (by calling 4 above) with the error value thrown.

  • If the onfulfilled handler returns a thenable (promise or promise like object), let's call it pObject, pObject needs to be set up pass on its settled state and value to p2 above.

    This is achieved by calling

    pObject.then( resolve2, reject2)
    

    so if pObject fulfills, its non-thenable success value is used to resolve p2, and if it rejects its rejection value is used to reject p2.

    The blog post defines its thenable's then method using parameter names based on how it is being used in the blog example (to resolve or reject a promise previously returned by a call to then on a native promise). Native promise resolve and reject functions are written in native code, which explains the first alert message.

5
Luciano Ferraz On

A thenable is any object containing a method whose identifier is then.

What follows is the simplest thenable one could write. When given to a Promise.resolve call, a thenable object is coerced into a pending Promise object:

const thenable = {
  then() {}, // then does nothing, returns undefined
};

const p = Promise.resolve(thenable);

console.log(p); // Promise { <pending> }

p.then((value) => {
  console.log(value); // will never run
}).catch((reason) => {
  console.log(reason); // will never run
});

The point of writing a thenable is for it to get coerced into a promise at some point in our code. But a promise that never settles isn't useful. The example above has a similar outcome to:

const p = new Promise(() => {}); //executor does nothing, returns undefined

console.log({ p }); // Promise { <pending> }

p.then((value) => {
  console.log(value); // will never run
}).catch((reason) => {
  console.log(reason); // will never run
});

When coercing it to a promise, JavaScript treats the thenable's then method as the executor in a Promise constructor (though from my testing in Node it appears that JS pushes a new task on the stack for an executor, while for a thenable's then it enqueues a microtask).

A thenable's then method is NOT to be seen as equivalent to promise's then method, which is Promise.prototype.then.

Promise.prototype.then is a built-in method. Therefore it's already implemented and we just call it, passing one or two callback functions as parameters:

// Let's write some callback functions...
const onFulfilled = (value) => {
  // our code
};
const onRejected = (reason) => {
  // our code
};

Promise.resolve(5).then(onFulfilled, onRejected); // ... and pass both to a call to then

The executor callback parameter of a Promise constructor, on the other hand, is not built-in. We must implement it ourselves:

// Let's write an executor function...
const executor = (resolve, reject) => {
  // our asynchronous code with calls to callbacks resolve and/or reject
};
const p = new Promise(executor); // ... and pass it to a Promise constructor
/* 
The constructor will create a new pending promise, 
call our executor passing the new promise's built-in resolve & reject functions 
as first and second parameters, then return the promise.

Whenever the code inside our executor runs (asynchronously if we'd like), it'll have 
said promise's resolve & reject methods at its disposal,
in order to communicate that they must respectivelly resolve or reject.
*/

A useful thenable

Now for a thenable that actually does something. In this example, Promise.resolve coerces the thenable into a promise:

const usefulThenable = {
  // then method written just like an executor, which will run when the thenable is
  // coerced into a promise
  then(resolve, reject) {
    setTimeout(() => {
      const grade = Math.floor(Math.random() * 11)
      resolve(`You got a ${grade}`)
    }, 1000)
  },
}

// Promise.resolve coerces the thenable into a promise
let p = Promise.resolve(usefulThenable)

// DO NOT CONFUSE the then() call below with the thenable's then.
// We NEVER call a thenable's then. Also p is not a thenable, anyway. It's a promise.
p.then(() => {
  console.log(p) // Promise { 'You got a 9' }
})
console.log(p) // Promise { <pending> }

Likewise, the await operator also coerces a thenable into a promise

console.log('global code has control')

const usefulThenable = {
  // then() will be enqueued as a microtask when the thenable is coerced
  // into a promise
  then(resolve, reject) {
    console.log("MICROTASK: usefulThenable's then has control")
    setTimeout(() => {
      console.log('TASK: timeout handler has control')
      const grade = Math.floor(Math.random() * 11)
      resolve(`You got a ${grade}`)
    }, 1000)
  },
}

// Immediately Invoked Function Expression
let p = (async () => {
  console.log('async function has control')
  const result = await usefulThenable //coerces the thenable into a promise
  console.log('async function has control again')
  console.log(`async function returning '${result}' implicitly wrapped in a Promise.resolve() call`)
  return result
})()

console.log('global code has control again')

console.log({ p }) // Promise { <pending> }

p.then(() => {
  console.log('MICROTASK:', { p }) // Promise { 'You got a 10' }
})

console.log('global code completed execution')

The output:

/*
global code has control
async function has control
global code has control again
{ p: Promise { <pending> } }
global code completed execution
MICROTASK: usefulThenable's then has control
TASK: timeout handler has control
async function has control again
async function returning 'You got a 10' implicitly wrapped in a Promise.resolve() call
MICROTASK: { p: Promise { 'You got a 10' } }
*/

TL;DR: Always write the thenable's then method as you would the executor parameter of a Promise constructor.