I think the short version of the question is: if the fulfillment handler supplied to a .then() returns a new promise, how does this new promise "unwrap" to the promise returned by .then()?  (unwrap seems like a terminology used in the promise technology). (and if it is not returning a new promise but just a "thenable", how does it unwrap?)
To chain several time-consuming asynchronous promises together, such as doing several network fetches described on this page or a series of animations, I think the standard method is stated as:
In the fulfillment handler passed to
.then(), create and return a new promisep, and in the executor passed to the constructor that createdp, do the time-consuming task, and resolve this new promisepwhen done.
Sometimes I may take this as: this is just the way it is (how to chain time-consuming promises), or it is the language feature, and you can just considered it to be happening by magic (if it is a language feature).
But is it true that this is done by standard procedure how it would be handled if the fulfillment handler returns a new promise?
I wrote out some rough draft of how it could be done below. And to state it in a few sentences, it is
- p1is the first promise.
- then()returns a new promise- p2
- the then()remembers whatp2is, as an internal property ofp1.
- when the fulfillment handler passed to theneventually get invoked (whenp1is resolved), the returned value is examined. If it is an object that has a property namedthenand is a function, then this object is treated as a thenable, which means it can be a genuine promise or just a dummy thenable (let's call itp1_New).
- Immediately, this is invoked: p1_New.then(() => { resolveP2() })
- Let's say p1_Newis a genuine promise. When we resolvep1_New, it will resolvep2, andp2will perform its "then" fulfillment handler, and the series of time-consuming promises can go on.
So this is the rough draft of code:
let p1 = new Promise(function(resolve, reject) {
    // does something that took 10 seconds
    resolve(someValue);
});
After the above code, the state of p1 is:
p1 = {
  __internal__resolved_value: undefined,
  __internal__state: "PENDING",
  __internal__executor: function() {    
    // this is the executor passed to the constructor Promise().
    // Something is running and the resolve(v) statement probably
    // will do
    // this.__internal__onResolve(v)
  },
  __internal__onResolve: function(resolvedValue) {
    if (this.__internal__then_fulfillHandler) {   // if it exists
      let vFulfill = this.__internal__then_fulfillHandler(this.__internal__resolved_value);
      // if the fulfillment handler returns a promise (a thenable),
      // then immediately set this promise.then(fn1)
      // where fn1 is to call this.__internal__resolveNewPromise()
      // so as to resolve the promise newPromise that I am returning
      if (vFulfill && typeof vFulfill.then === "function") { // a promise, or thenable
        vFulfill.then(function() {
          this.__internal__resolveNewPromise(this.__internal__resolved_value)
        })
      }
    }
  },
  // in reality, the then should maintain an array of fulfillmentHandler
  //   because `then` can be called multiple times
  then: function(fulfillmentHandler, rejectionHandler) {
    this.__internal__then_fulfillHandler = fulfillmentHandler;
    // create a new promise newPromise to return in any case
    let newPromise = new Promise(function(resolve, reject) {
      this.__internal__resolveNewPromise = resolve;
    });
    // if promise already resolved, then call the onResolve 
    //   to do what is supposed to be done whenever this promise resolves
    if (this.__internal__state === "RESOLVED") {
      this.__internal__onResolve(this.__internal__resolved_value);
    }
    return newPromise;
  }
}
and that's how when p1_New is resolved, then p2 is resolved, and the fulfillment handler passed to p2.then() will go on.
let p2 = p1.then(function() {
  p1_New = new Promise(function(resolve, reject) {
    // does something that took 20 seconds
    resolve(someValue);
  });
  return p1_New;
});
p2.then(fulfillmentHandler, rejectionHandler);
But what if p1_New is not really a promise, but just a dummy thenable written this way:
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
it is doing the time-consuming task in its then(). That's fine too: if we look at step 5 above:
- Immediately, this is invoked: p1_New.then(() => { resolveP2() })
that is, then is invoked immediately, and with the first function passed in being able to resolve p2. So that thenable performs some time-consuming task, and when done, call its first parameter (the function that resolves p2), and the whole sequence can go on like before.
But in this case, it is the then() doing the time-consuming task, not the executor doing it (the executor passed to the constructor of a new promise). In a way, it is like the then() has become the executor.
