2

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

tadman
  • 208,517
  • 23
  • 234
  • 262
MaximilianMairinger
  • 2,176
  • 2
  • 15
  • 36
  • Do you want an `async` function that returns a Promise when you call `await` on it? It's not clear what the confusion is. – tadman May 12 '20 at 21:34
  • 1
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function - note this section: `If the return value of an async function is not explicitly a promise, it will be implicitly wrapped in a promise.`. So if the return value is already a promise, it isn't wrapped. – Robin Zigmond May 12 '20 at 21:36
  • 4
    If you want a promise to resolve to a promise, then you HAVE to wrap the inner one in an object so the first promise resolves to an object that contains a promise. That's true whether it's `Promise.resolve(somePromise)`, returning a promise from a `.then()` handler or returning a promise in an `async` function. It's how it is designed. – jfriend00 May 12 '20 at 21:38
  • 1
    Of interest is, why? It is designed the way it is because you pretty much always want promises to chain and for the parent promise to wait for child promises. What is your use case for not having the parent and child promises chained together? – jfriend00 May 12 '20 at 21:39
  • No. Flattening works on every thenable (object with a `.then` method). – Jonas Wilms May 12 '20 at 21:41
  • Promises in JavaScript follow the A+ spec. [It says](https://promisesaplus.com/#point-56) that if the resolved value of a promise is a thenable (ie. has a function named then), then the promise chains will be linked. I think. In this way you avoid a nesting of promises, and instead construct an implicit resolution chain. If I am right, then a way to circumvent this could be engineered to avoid `.then` being seen as a function. In this way the promise itself would be returned as the fulfilled value of the promise. Maybe. A revocable proxy might be of use? – Ben Aston May 12 '20 at 22:50
  • @jfriend00 I have a indirect recursive function which depends on the first promise beeing resolved before continuing (used in a **prioritised** preemptive loading architecture, hence it waits). When awaiting both promises in the main loop, it blocks itself. Anyway out of scope for this question. I spent a good while debugging this, since it never occured to me that this would implicitly happen.. Very unintuitive IMO. – MaximilianMairinger May 13 '20 at 11:15

1 Answers1

6

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.

tadman
  • 208,517
  • 23
  • 234
  • 262
  • @JonasWilms The way to avoid this is to not create the problem in the first place. If you want a raw promise, don't use `async` to declare it, that just goes against the grain. – tadman May 12 '20 at 21:45
  • " I would really like to have a **nested** Promise" ... I think it's clear from the question that this is not about async / await. – Jonas Wilms May 12 '20 at 21:50
  • 1
    @JonasWilms "Sorry, JavaScript doesn't want to work that way." I added a note about how even using `Promise.resolve()` won't help since that will automatically unwrap it. Everything about Promises is designed to work against this goal. – tadman May 12 '20 at 21:50
  • 1
    Yes, and that's now an answer to the question – Jonas Wilms May 12 '20 at 21:53