5

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
nonopolarity
  • 146,324
  • 131
  • 460
  • 740
  • 4
    It’s just using a confusing pair of names. The meaning is no different, and the implementation of `then` is not a very good idea for a real-world thenable (it doesn’t act like a promise). – Ry- Dec 26 '19 at 19:57
  • 1
    Also note that in vanilla JavaScript there is no way to test the interface. So as soon as an object has a `then` method, it passes as a thenable. That method does not even have to have declared parameters. And it can do whatever it wants to. It is a thenable. – trincot Dec 26 '19 at 19:59
  • Also note that calling `resolve` on a Promise does not directly call the `fullfillmentHandler`. – Jonas Wilms Dec 26 '19 at 20:02
  • Do we mean `then(fulfillmentHandler, rejectionHandler)` and `then(resolve, reject)` are the same thing? So `fulfillmentHandler` is actually a `resolve`? (or `resolveHandler`?) I take it for `new Promise(function(f, g) { ... }`, the JS engine invoke this executor immediately and passes in some native code to resolve something as `f` or `resolve`... we never invoke `then(f, g)` with `f` being a `resolve` – nonopolarity Dec 26 '19 at 20:15
  • yes. You can also attach multiple `fullfilmentHandlers`, but the promise `resolve`s only once. – Jonas Wilms Dec 26 '19 at 20:46

3 Answers3

1

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.

  • "*JavaScript treats the thenable's then method as the executor in a Promise constructor*" - not quite. Coercing to a promise (e.g. using `Promise.resolve(usefulThenable)`) will do defer to the `resolve` function of a promise constructor, which may pass `resolve` and `reject` callbacks as arguments to your `then` implementation, but it still does call it as a method, unlike the executor callback. – Bergi Jul 10 '22 at 00:56
  • "*We NEVER call a thenable's then*" - there's nothing wrong with calling `usefulThenable.then()` directly. Some implementations may even provide a useful return value. This also conflicts with "*Always write the thenable's `then` method as you would the `executor` parameter of a Promise constructor.*" - a `then` implementation should not throw synchronously, and it may return a value. – Bergi Jul 10 '22 at 00:59
  • So you're saying that a thenable `then` implementation can do stuff other than being used for coercion. So as long as it accepts the callback parameters of an `executor`, and works like an `executor` when needed, we can be creative with it for other purposes. For example: while any value returned inside an `executor` is ignored, a `then` could do more than imitate an executor, so on occasion it might be used as a regular method whose return value has usefulness. – Luciano Ferraz Jul 10 '22 at 17:08
  • _"a then implementation should not throw synchronously"_ - I don't get what you mean by throwing _synchronously_, as there is no _asynchronous throw_. Also, MDN's page on `Promise.resolve()` has a `then` implementation throwing: [Thenable throws before callback](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve#resolving_thenables_and_throwing_errors) – Luciano Ferraz Jul 10 '22 at 17:09
  • "*on occasion it might be used as a regular method whose return value has usefulness*" - yes, exactly that. And to be useful, it shouldn't `throw`, it should call the `onRejected` callback instead - although if it did, `resolve` would know how to handle that – Bergi Jul 10 '22 at 18:40
0

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).

nonopolarity
  • 146,324
  • 131
  • 460
  • 740
  • `If any JavaScript engine works slightly differently, then the results can change` nope. Then it is not a valid ES262 engine. – Jonas Wilms Dec 27 '19 at 11:09
  • "So it is still best not to return a thenable that has the interface" for sure you can do that. – Jonas Wilms Dec 27 '19 at 11:09
  • it just feels a bit hacky but it seems working with other frameworks, sometimes you can make use of a thenable like that – nonopolarity Dec 27 '19 at 11:17
  • It's not hacky at all. That's the thing. – Jonas Wilms Dec 27 '19 at 11:21
  • So maybe in the promise community, this is the standard way to chain promises and people take this fact as granted, can you say. – nonopolarity Dec 27 '19 at 11:29
  • No. It's *specified*. The situations were Promises don't work are rare, but they exist. In those cases it is totally fine to build your own thenable (e.g. the MongoDB driver uses a thenable query, so at the point you await it, the query executes) – Jonas Wilms Dec 27 '19 at 11:32
0

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.

traktor
  • 17,588
  • 4
  • 32
  • 53
  • nice analysis. so I think it is, since the promise system is written in JS also, we can deduce that at some point, `pObject.then( resolve2, reject2)` has to be called, and therefore can make use of this fact? In a way, it is like digging into the blackbox and conclude some property of it and make use of it. I think in this situation it makes sense. Maybe this practice is not recommended by some if they view the abstraction (the blackbox) not something you should guess about. – nonopolarity Dec 27 '19 at 04:37
  • 1
    If a programming language has promise built in and is done by some keyword or syntax, what happens can just happen by "magic". And the usage of `then(resolve, reject)` adds some confusion because it is not the usual interface: `resolve, reject` is provided to the function that is passed to the promise constructor, not to `then()`. But I guess next time we see `then(resolve, reject)`, we can think about somebody passing a resolveHandler as the fulfillmentHandler, to resolve something. – nonopolarity Dec 27 '19 at 04:40
  • @nopole Prior to the introduction of ECMAScript 2015 (a.k.a ES6) all promises were written in JavaScript, usually as part of a larger library. Some of these libraries are still in use. All Promise pollyfills for older browsers are, of course, also written in JavaScript. Native Promise implementations are likely to be written in native code. A major design goal of promises was to hide the implementation of promise logic from the user. I have benefited from understanding how promises actually work, but the are explicitly designed to _prevent_ changing their designed behavior from user code. – traktor Dec 27 '19 at 06:42
  • @nopole Techniclly, resolving a promise with a promise reaches enqueuing a [PromiseResolveThenableJob](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-promise-resolve-functions) in the 2019 ECMAScript standard. As per step 2 of the link and following note, the thenable's `then` method is called to resolve or reject `p2`. I think it likely the blog named parameters informed by the nature of running a "PromiseResolveThenableJob". See also the [A+ promises](https://promisesaplus.com) spec for an earlier description of promise resolution. – traktor Dec 27 '19 at 06:45