Understanding what triggers moving to the next .then() of a javascript promise chain

Viewed 193

I had a promise chain that looked something like this.

fistAsyncFunction()
    .then(() => {
        secondAsyncFunction()
    })
    .then(() => {
        thirdAsyncFunction()
    })

I noticed that the third async function was starting before the second async function finished, but after changing it to this, it worked how I wanted it to.

fistAsyncFunction()
    .then(() => {
        return secondAsyncFunction()
    })
    .then(() => {
        thirdAsyncFunction()
    })

It seems like even though the second async function didn't return anything, the program waited for it to return undefined before moving onto the next promise. Is this what's actually happening? If not, then what is going on under the hood that prompts javascript to start executing the next promise?

2 Answers

Think about it this way

fistAsyncFunction()
    .then(() => {
        secondAsyncFunction()
        return undefined
    })
    .then((iAmUndefined) => {
        thirdAsyncFunction()
        return undefined
    })

^ This is your first example with explicit returns.

It seems like even though the second async function didn't return anything, the program waited for it to return undefined before moving onto the next promise.

So you are sort of right on this one. The second async function does not return anything; that is correct. The program, however, does not wait for secondAsyncFunction() to return undefined. Moreover, it does not wait for secondAsyncFunction() at all. Instead, your program is saying

Okay secondAsyncFunction I will now call you and let you run away; I will not wait for you. Instead I'm just going to return undefined and move on. Hello thirdAsyncFunction!

You are correct in noticing that your thirdAsyncFunction is started before your secondAsyncFunction returned.

Next, you made an amendment to your code to achieve your expected behavior

fistAsyncFunction()
    .then(() => {
        return secondAsyncFunction()
    })
    .then(resultOfSecondAsyncFunction => {
        thirdAsyncFunction()
        return undefined
    })

^ this is your second example with explicit returns

Your program is now working the way you want it to because by returning secondAsyncFunction(), you are now instructing your program to wait for the return of that function.

what is going on under the hood that prompts javascript to start executing the next promise?

Under the hood, there is a special function resolve that is used in the Promise constructor. The next promise will start executing when the resolve function of the previous promise is called. Of course, both promises must be tied together by a .then.

When you have a promise chain like this:

fn1().then(() => {
    fn2();
}).then(() => {
    fn3();
});

Here's the sequence of events:

  1. fn1() executes and returns a promise (I will call p1).
  2. Using that returned promise p1, the .then() method is called which simply stores a callback and immediately returns another promise p2.
  3. Using that returned promise p2, the .then() method is called which simply stores a callback and immediately returns another promise p3 (which is not saved or used by anything).
  4. Sometime later p1 resolves. That causes the previously saved .then() handler to get called.
  5. That .then() handler runs the code inside it. In this specific example, this calls fn2(). But, since fn2() is also asynchronous, it just starts its operation and immediately returns a promise. But, in this particular code example, nothing is looking at that returned promise so it is not used. Then, execution returns from the .then() handler back to the promise that originally called it. That promise looks at the return value from the .then() handler and finds it undefined. Since there's no additional promise here to wait for, it then resolves its own promise p2.
  6. When p2 is resolved, it calls its .then() handler which starts fn3(). You will now have both fn2() and fn3() in flight at the same time and it will be somewhat indeterminate which one completes first (it's really just a race between their two asynchronous operations).

When you instead do it like this:

fn1().then(() => {
    return fn2();
}).then(() => {
    return fn3();
});

Then, everything is the same up until step 5. But, in step 5 for this example, the first .then() handler does return fn2(). So, fn2() gets called just like in the previous example and it returns a promise and that promise is returned from the .then() handler. That tells the host promise that it needs to wait until THAT promise also resolves before it continue the promise chain.

So, if you run asynchronous operations inside a .then() handler and you want the promise chain to wait for them, you have to return a promise from the .then() handler that is linked to your internal asynchronous operations. If you don't, then those additional asynchronous operations are just "fire and forget" as nothing is paying attention to when they complete or if they have an error.

Related