Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Promises - How to make asynchronous code execute synchronous without async / await?

  var p1 = new Promise(function(resolve, reject) {  
    setTimeout(() => resolve("first"), 5000);
  });
  var p2 = new Promise(function(resolve, reject) {  
    setTimeout(() => resolve("second"), 2000);
  });
  var p3 = new Promise(function(resolve, reject) {  
    setTimeout(() => resolve("third"), 1000);
  });

  console.log("last to print");

p1.then(()=>p2).then(()=>p3).then(()=> console.log("last to be printed"))

As I was reading about promises, I know that I can print promises synchronous (in this case print: first, second, third, last to print) when I use async /await. Now I have also been reading that the same thing can be achieved using .then chaining and async/await is nothing 'special'. When I try to chain my promises, however, nothing happens except for the console.log of "last to be printed". Any insight would be great! Thanks!!

Edit to question:

  var p1 = new Promise(function (resolve, reject) {
    setTimeout(() => console.log("first"), 5000);
    resolve("first resolved")
  });
  var p2 = new Promise(function (resolve, reject) {
    setTimeout(() => console.log("second"), 2000);
    resolve("second resolved")
  });
  var p3 = new Promise(function (resolve, reject) {
    setTimeout(() => console.log("third"), 0);
    resolve("third resolved")
  });

  console.log("starting");
  p1.then((val) => {
    console.log("(1)", val)
    return p2
  }).then((val) => {
    console.log("(2)", val)
    return p3
  }).then((val) => {
    console.log("(3)", val)
  })

Loggs:

starting
(1) first resolved
(2) second resolved
(3) third resolved
third
second
first

1: if executor function passed to new Promise is executed immediately, before the new promise is returned, then why are here promises resolved ()synchronously) first and after the setTimeouts (asynchronously) gets executed?

  1. Return value vs. resolve promise:

    var sync = function () { return new Promise(function(resolve, reject){ setTimeout(()=> { console.log("start") resolve("hello") //--works // return "hello" //--> doesnt do anything }, 3000); }) } sync().then((val)=> console.log("val", val))

like image 364
javascripting Avatar asked Dec 10 '22 04:12

javascripting


1 Answers

The executor function you pass to new Promise is executed immediately, before the new promise is returned. So when you do:

var p1 = new Promise(function(resolve, reject) {  
  setTimeout(() => resolve("first"), 5000);
});

...by the time the promise is assigned to p1, the setTimeout has already been called and scheduled the callback for five seconds later. That callback happens whether you listen for the resolution of the promise or not, and it happens whether you listen for resolution via the await keyword or the then method.

So your code starts three setTimeouts immediately, and then starts waiting for the first promise's resolution, and only then waiting for the second promise's resolution (it'll already be resolved, so that's almost immediate), and then waiting for the third (same again).

To have your code execute those setTimeout calls only sequentially when the previous timeout has completed, you have to not create the new promise until the previous promise resolves (using shorter timeouts to avoid lots of waiting):

console.log("starting");
new Promise(function(resolve, reject) {  
  setTimeout(() => resolve("first"), 1000);
})
.then(result => {
    console.log("(1) got " + result);
    return new Promise(function(resolve, reject) {  
      setTimeout(() => resolve("second"), 500);
    });
})
.then(result => {
    console.log("(2) got " + result);
    return new Promise(function(resolve, reject) {  
      setTimeout(() => resolve("third"), 100);
    });
})
.then(result => {
    console.log("(3) got " + result);
    console.log("last to print");
});

Remember that a promise doesn't do anything, and doesn't change the nature of the code in the promise executor. All a promise does is provide a means of observing the result of something (with really handy combinable semantics).

Let's factor out the common parts of those three promises into a function:

function delay(ms, ...args) {
    return new Promise(resolve => {
        setTimeout(resolve, ms, ...args);
    });
}

Then the code becomes a bit clearer:

function delay(ms, ...args) {
    return new Promise(resolve => {
        setTimeout(resolve, ms, ...args);
    });
}

console.log("starting");
delay(1000, "first")
.then(result => {
    console.log("(1) got " + result);
    return delay(500, "second");
})
.then(result => {
    console.log("(2) got " + result);
    return delay(100, "third");
})
.then(result => {
    console.log("(3) got " + result);
    console.log("last to print");
});

Now, let's put that in an async function and use await:

function delay(ms, ...args) {
    return new Promise(resolve => {
        setTimeout(resolve, ms, ...args);
    });
}

(async() => {
    console.log("starting");
    console.log("(1) got " + await delay(1000, "first"));
    console.log("(2) got " + await delay(500, "second"));
    console.log("(3) got " + await delay(100, "third"));
    console.log("last to print");
})();

Promises make that syntax possible, by standardizing how we observe asynchronous processes.


Re your edit:

1: if executor function passed to new Promise is executed immediately, before the new promise is returned, then why are here promises resolved ()synchronously) first and after the setTimeouts (asynchronously) gets executed?

There are two parts to that question:

A) "...why are here promises resolved ()synchronously) first..."

B) "...why are here promises resolved...after the setTimeouts (asynchronously) gets executed"

The answer to (A) is: Although you resolve them synchronously, then always calls its callback asynchronously. It's one of the guarantees promises provide. You're resolving p1 (in that edit) before the executor function returns. But the way you're observing the resolutions ensures that you observe the resolutions in order, because you don't start observing p2 until p1 has resolved, and then you don't start observing p3 until p2 is resolved.

The answer to (B) is: They don't, you're resolving them synchronously, and then observing those resolutions asynchronously, and since they're already resolved that happens very quickly; later, the timer callbacks run. Let's look at how you create p1 in that edit:

var p1 = new Promise(function (resolve, reject) {
  setTimeout(() => console.log("first"), 5000);
  resolve("first resolved")
});

What happens there is:

  1. new Promise gets called
  2. It calls the executor function
  3. The executor function calls setTimeout to schedule a callback
  4. You immediately resolve the promise with "first resolved"
  5. new Promise returns and the resolved promise is assigned to p1
  6. Later, the timeout occurs and you output "first" to the console

Then later you do:

p1.then((val) => {
  console.log("(1)", val)
  return p2
})
// ...

Since then always calls its callback asynchronously, that happens asynchronously — but very soon, because the promise is already resolved.

So when you run that code, you see all three promises resolve before the first setTimeout callback occurs — because the promises aren't waiting for the setTimeout callback to occur.

You may be wondering why you see your final then callback run before you see "third" in the console, since both the promise resolutions and the console.log("third") are happening asynchronously but very soon (since it's a setTimeout(..., 0) and the promises are all pre-resolved): The answer is that promise resolutions are microtasks and setTimeout calls are macrotasks (or just "tasks"). All of the microtasks a task schedules are run as soon as that task finishes (and any microtasks that they schedule are then executed as well), before the next task is taken from the task queue. So the task running your script does this:

  1. Schedules a task for the setTimeout callback
  2. Schedules a microtask to call p1's then callback
  3. When the task ends, its microtasks are processed:
    1. The first then handler is run, scheduling a microtask to run the second then handler
    2. The second then handler runs and schedules a micro task to call the third then handler
    3. Etc. until all the then handlers have run
  4. The next task is picked up from the task queue. It's probably the setTimeout callback for p3, so it gets run and "third" appears in the console
  1. Return value vs. resolve promise:

The part you've put in the question doesn't make sense to me, but your comment on this does:

I read that returning a value or resolving a promise is same...

What you've probably read is that returning a value from then or catch is the same as returning a resolved promise from then or catch. That's because then and catch create and return new promises when they're called, and if their callbacks return a simple (non-promise) value, they resolve the promise they create with that value; if the callback returns a promise, they resolve or reject the promise they created based on whether that promise resolves or rejects.

So for instance:

.then(() => {
    return 42;
})

and

.then(() => {
    return new Promise(resolve => resolve(42));
})

have the same end result (but the second one is less efficient).

Within a then or catch callback:

  1. Returning a non-promise resolves the promise then/catch created with that value
  2. Throwing an error (throw ...) rejects that promise with the value you throw
  3. Returning a promise makes then/catch's promise resolve or reject based on the promise the callback returns
like image 62
T.J. Crowder Avatar answered May 23 '23 14:05

T.J. Crowder