imheretolearn1
imheretolearn1

Reputation: 137

LONG: Why if we return a promise from our async function, its not immediately resolved?

Apologies in advance if this is a dumb question, I've been trying to find the answer to my question for a good while now but still no luck and that's causing me not to progress further in my JS learning!

Q: Why even if we return a resolved promise from our async function, it's not immediately resolved?

Now MDN is teaching me this:

Async functions always return a promise. If the return value of an async function is not explicitly a promise, it will be implicitly wrapped in a promise.

This means, both the functions below f1 and f2, are equivalent!

async function f1(){
    
    return 100;
}
 
    async function f2(){
    
    return Promise.resolve(100);
}
    
    console.log(f1());
    console.log(f2());

However, we get different logs:

One logs: Promise {: 100}

Second logs: Promise pending

What's even more confusing is, when I expand the second promise's, even tho it says its pending, i see [[PromiseResult]]: 100.

Now my naive programming experience suggests, only if a value (string, num, arr, obj etc) is returned from the async function, the promise our async function returns by default will be resolved to that value!

But if our async function returns a promise that we ourself made inside our async function, then the default promise that async function returns wont be returned, instead the promise we made inside our async function will be returned.

But that raises another question, what if we have await in our function? Await immediately returns a pending promise and whatever value we return from our async function is resolved with the promise our async function returns but again, what if we're returning a function?

A friend of mine told me if we are returning a resolved promise from our async function, some anonymous task is registered into the microtask queue which when run (after all the sync code,) unpacks the value from our the secondary promise and finally resolves our default promise returned by async function with that value and that seems to be working!

To test this assumption, I wrote this code:

   let o = new Promise(function (r){
    r(100)
});

o.then(function first(){console.log(p)})
.then(function second(){console.log(p)})

async function f(){

return new Promise(function (r){
    r(100)
})
}

let p = f();

Our o promise is gonna be resolved immediately so our first .then cb will be pushed to the microtask queue, like this maybe!

MICROTASK_QUEUE = [function first(){console.log(p)}]

On the next line we run f(), it goes inside f and sees we're returning a resolved promise so it returns a pending promise into p!

Now our microtask is like this:

MICROTASK_QUEUE = [function first(){console.log(p)}, someTaskThatWillUnpackOurPromiseAndResolveOurPromiseReturnedByTheAsyncFunctionByThatValue]

Our sync code is done, so we run our first task from microtask queue which happens to be our first .then, this one: function first(){console.log(p)}

This one is run and as expected, prints pending! Since this returns nothing, it returns undefined but since the promise's root .then is resolved, now our microtask queue looks like this after registering another .then.

MICROTASK_QUEUE = [someTaskThatWillUnpackOurPromiseAndResolveOurPromiseReturnedByTheAsyncFunctionByThatValue, function second(){console.log(p)}]

Now, this function is run someTaskThatWillUnpackOurPromiseAndResolveOurPromiseReturnedByTheAsyncFunctionByThatValue() (maybe internally unlike our .then functions)

now at this point promise that was earlier returned by our async function should be resolved with the value we resolved in our custom-made function, correct?

Now finally event loop checks the microtask queue and finds the final function to run, this one:

function second(){console.log(p)}

now why does this still return a pending promise?

I think there's still one more step I'm missing in this whole equation because if I put the 3rd .then, it finally logs this "Promise {: 100}" which is what I'm after!

let o = new Promise(function (r){
    r(100)
});

o.then(x => console.log(p))
.then(x => console.log(p))
.then(x => console.log(p))
async function f(){

return new Promise(function (r){
    r(100)
})
}

let p = f();

I'm thankful to you if you read this far and it'd be greatly appreciated if someone could answe this

Upvotes: 0

Views: 1840

Answers (1)

Bergi
Bergi

Reputation: 664548

You should ignore the exact timing and ordering of the microtask jobs, it is not relevant for writing code - if you have dependencies between sections of asynchronous code, make them explicit, do not rely on queuing.

But it's fun to look at the internals of the JS engine anyway, so…

But if our async function returns a promise that we ourself made inside our async function, then the default promise that async function returns wont be returned, instead the promise we made inside our async function will be returned.

No, that's not what happens. Your doubt about await followed by a returned promise brought you back on track - it always resolves the default promise with the return value, whatever that value is and whenever the return evaluates (immediately vs. asynchronously). Your friend is right.

Now, this function is run: someTaskThatWillUnpackOurPromiseAndResolveOurPromiseReturnedByTheAsyncFunctionByThatValue() (maybe internally unlike our .then functions)

now at this point promise that was earlier returned by our async function should be resolved with the value we resolved in our custom-made function, correct?

Not quite. You assumed that this unpacking happens unlike our .then() functions. But this is a potential optimisation that ES6 did fail to specify. Instead, it happens exactly like calling the .then() method - and not just "like": it indeed just does call the .then() method, which is necessary to resolve ordinary thenables, without taking a look whether the target is a native promise or a thenable.

And what the invoked .then(resolve, reject) method does is to schedule yet another job with the callback, which causes the outer promise to be resolved only after one additional tick.

You can compare the timings between the following functions:

const fulfilled = Promise.resolve('a');
async function a() {
  return {
    then(onFulfillment, onRejection) {
      console.log('promise then');
      fulfilled.then(onFulfillment, onRejection);
    }
  };
}
async function b() {
  return {
    then(onFulfillment, onRejection) {
      console.log('immediate then');
      onFulfillment('b');
    }
  };
}

Upvotes: 4

Related Questions