0

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 promise p, and in the executor passed to the constructor that created p, do the time-consuming task, and resolve this new promise p when 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

  1. p1 is the first promise.
  2. then() returns a new promise p2
  3. the then() remembers what p2 is, as an internal property of p1.
  4. when the fulfillment handler passed to then eventually get invoked (when p1 is resolved), the returned value is examined. If it is an object that has a property named then and 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 it p1_New).
  5. Immediately, this is invoked: p1_New.then(() => { resolveP2() })
  6. Let's say p1_New is a genuine promise. When we resolve p1_New, it will resolve p2, and p2 will 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:

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

nonopolarity
  • 146,324
  • 131
  • 460
  • 740
  • 4
    It's hard to tell what you're really asking here. FYI, you shouldn't think of chaining promises as having anything to do with time consuming operations. It's about tracking the results of asynchronous operations (whether time consuming or not). And a synchronous time consuming operation derives little benefit from using a promise. – jfriend00 Dec 27 '19 at 18:34
  • that's true... I should edit the question... I tried a promise that calculated `60000!` (factorial) and it would block everything in Node and in a browser (Google Chrome). What I should state probably is time-consuming asynchronous tasks. – nonopolarity Dec 27 '19 at 18:38
  • Promises have nothing to do with "time consuming". They are about asynchronous tasks of ANY duration. You can wrap a synchronous tasks in a promise, but as you've already discovered, it doesn't make them non-blocking, it still blocks the JS event loop during its synchronous execution. – jfriend00 Dec 27 '19 at 18:41
  • Not even async... `Promise.resolve(1)` returns a promise... Try `Promise.resolve(1).then(n => console.log(n));`. – Heretic Monkey Dec 27 '19 at 18:45
  • 1
    You've got a lot of suppositions and assumptions. Why not just look at the code of the Bluebird or Q libraries, that allowed for promises to be used before they were built into browsers? – Heretic Monkey Dec 27 '19 at 18:47
  • @jfriend00 so the promise executor must be doing something non-blocking, such as fetching data in a non-blocking manner (ajax, fetch, or `script.src = "..."`), or setTimeout(), or jQuery's animate(), which is probably a setInterval – nonopolarity Dec 27 '19 at 18:48
  • I'm not sure if this is what you are asking: But JavaScript is not multithreaded. And Promises do not magically make code none blocking. So moving a blocking code as-is into a `then` callback will still result in blocking code, just at a different point in time. With the `await`/`async` syntax you can utilize `await` so that other code can interleave at that point. So you basically do _cooperative multitasking_. – t.niese Dec 27 '19 at 18:49
  • The promise executor is called synchronously so if you put a long synchronous activity in there, it just blocks everything until that returns. A promise is useful when you put an asynchronous operation in the executor. You initiate the asynchronous operation and then return quickly (`setTimeout()` would be the canonical example). Meanwhile, the asynchronous operation (using native code) runs independent of the JS interpreter and because the executor returned, other JS can now run. – jfriend00 Dec 27 '19 at 18:54
  • cont'd... Sometime later, the asynchronous operation completes, adds a callback event to the JS event queue and, when that event gets serviced by the interpreter, it can call `resolve()` or `reject()` as appropriate and continue the promise chain to the next link in the chain. – jfriend00 Dec 27 '19 at 18:54
  • @t.niese I think a promise typically relies on some operation that is a system thread, like an ajax operation that silently does it in the background and notifies about the completion, or CSS transition that is done in a separate thread, and using a `setTimeout` or `on("transitionend")` to get notified. So it is like handing off a complete task to another thread and get notified when that lengthy task is done (perhaps with some result) – nonopolarity Dec 27 '19 at 18:57
  • 1
    I am not sure what you are really asking in this question. If it is to check that your understanding of the [Promise Resolution Procedure](https://promisesaplus.com/#the-promise-resolution-procedure) is correct, you can have your implementation code tested against a [Promises/A+ Compliance Test Suite](https://github.com/promises-aplus/promises-tests). I did this at the time for my [own implemenatation](https://stackoverflow.com/questions/23772801/basic-javascript-promise-implementation-attempt/42057900#42057900). – trincot Dec 27 '19 at 20:09
  • my question is about, what makes chaining a series of promises possible, with each one resolving after an indefinite amount of time... – nonopolarity Dec 27 '19 at 20:11
  • Well, I am sure you understand that is because each `then` call on a promise will register the `then`-callback in the promise's internal state, so that it can be called later, when the promise resolves. This makes the chain work, no? – trincot Dec 27 '19 at 20:16

1 Answers1

0

I understand promises more now, it seems this is the standard way.

Whatever the fulfillment handler returns: a primitive value, an object that is not a thenable, a thenable that is like a fake promise, or a real promise, will all be set up so that it will resolve p, where p is the promise that is returned by then.

This is the standard procedure, and what is called unwrapping. We can wrap and wrap a promise by many layers, when it unwraps from the inside, it will unwrap until it cannot continue.

And it is true, when an thenable fake promise, or a real promise is returned, it will trigger this action to happen underneath:

obj.then(resolveP) where resolveP is the resolve function that can resolve p.

That how that promise resolves p, and how the thenable can do asynchronous task and use the first argument passed to it as a resolve function to "resolve" p.

nonopolarity
  • 146,324
  • 131
  • 460
  • 740