JS: async function implicitly unwraps promise?

Viewed 1375

To the best of my knowledge async functions wrap their returned value into a promise implicitly. This does work for every property, except Promises themselfs.

async function f() {
  return new Promise((res) => {
    setTimeout(() => {
      res("Why am I being unwrapped")
    }, 1000)
  })
}

(async () => {
  console.log(await f())
})()

Those get unwrapped before being returned. So that await f() actually awaits two nested promises.

Note that this also works with explicitly createded Promises (Promise.resolve(new Promise(...)))

Is there a good way to avoid this? I would really like to have a nested Promise without a quickfix like so.

async function y() {
  return {wrapped: new Promise((res) => {
    setTimeout(() => {
      res("Why am I being unwrapped")
    }, 1000)
  })}
}

(async () => {
  console.log((await y()).wrapped)
})()

Sandbox

1 Answers

As per the MDN documentation on async functions:

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.

In other words, since you're returning a Promise it does not get wrapped. If you want to re-wrap it you could, but you could also strip the async keyword which has a similar effect. You get the raw Promise back unless you await it.

So effectively:

function f() {
  return new Promise((res) => {
    setTimeout(() => {
      res("Might be wrappers on some of you, but not on me!")
    }, 1000)
  })
}

(async () => {
  console.log(f())
})()

Which gives you output like:

Promise { <pending> }

If the goal is to have a Promise that returns a raw Promise then you're going to be fighting against everything that the Promise system is designed to do, namely automatically unwrap Promises as they're discovered. Promise.resolve() wraps everything but Promises, and if you give it a Promise somehow, it goes about unwrapping that recursively.

You can do this by returning something that is not a Promise, like in your example, but that does seem to defeat the entire purpose of Promises.

If you have a particular problem you're trying to solve that absolutely necessitates a deferred Promise, consider returning a function instead of some arbitrary object:

function f() {
  return () => {
    return new Promise((res) => {
      setTimeout(() => {
        res("I'll start as soon as you ask.")
      }, 1000)
    })
  };
}

Then you can call let p = f() and later p() when you want to get an actual Promise. This is how JavaScript normally handles deferred execution.

If you want to initiate the execution on the Promise immediately, you can still accommodate that:

function f() {
  const p = new Promise((res) => {
    setTimeout(() => {
      res("Already underway when requested.")
    }, 1000)
  })

  return () => { p };
}

Though these sorts of techniques are usually best avoided if at all possible, and usually they are.

Related