Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save tivrfoa/f9d130a40bcf7e42ddc8a1472eb3e5fe to your computer and use it in GitHub Desktop.

Select an option

Save tivrfoa/f9d130a40bcf7e42ddc8a1472eb3e5fe to your computer and use it in GitHub Desktop.
JavaScript - Auto-Unwrapping of Promises in `async` Functions

Prompting ChatGPT:

I think the following is a strange behavior in JavaScript:

Promises auto-unwrap, so if you do return a promise for a value from within an async function, you will receive a promise for the value (not a promise for a promise for the value).

function defer(callback) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve(callback());
    }, 1000);
  });
}

async function increment(num) {
  // It doesn't matter whether you put an await here.
  return defer(() => num + 1);
}

// Logs: 4
increment(3).then(num => console.log(num));

ChatGPT

Yes, this behavior might seem strange at first, but it's actually a key feature of how async functions and Promises work in JavaScript:

Auto-Unwrapping of Promises in async Functions

When you return a Promise inside an async function, JavaScript automatically unwraps it.
That means:

  • If an async function returns a non-Promise value, it gets wrapped in Promise.resolve().
  • If an async function returns a Promise, JavaScript automatically flattens it.

Step-by-Step Breakdown of Your Example

function defer(callback) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve(callback());  // Resolving with num + 1
    }, 1000);
  });
}

async function increment(num) {
  // `defer()` returns a Promise.
  // Since `increment` is `async`, returning a Promise means it is auto-unwrapped.
  return defer(() => num + 1);
}

// Call the function and log the result
increment(3).then(num => console.log(num)); // Logs: 4

Why Does This Happen?

  1. defer() returns a Promise

    • defer(() => num + 1) creates a delayed Promise that resolves to num + 1 after 1 second.
  2. The async function increment(num) returns that Promise

    • Because increment is async, it does not return a Promise inside another Promise (no nesting).
    • Instead, JavaScript unwraps the returned Promise automatically.
  3. When calling increment(3)

    • It immediately returns a Promise that resolves to 4, not a Promise<Promise<number>>.
  4. .then(num => console.log(num)) receives 4, not another Promise.


What Would Happen Without async?

If increment were a normal function:

function increment(num) {
  return defer(() => num + 1);
}

Then calling increment(3) still returns a Promise, and .then(console.log) still logs 4,
but the behavior is clearer because there's no async function involved.


What If We Used await?

If you explicitly await the Promise before returning, the result remains the same:

async function increment(num) {
  return await defer(() => num + 1);
}
  • Here, await pauses execution until the Promise resolves.
  • But since await defer(...) already gives num + 1, the return is just wrapping it in a Promise again.

So await here makes no difference!


Key Takeaways

βœ… Async functions auto-unwrap Promises
βœ… Returning a Promise from an async functiondoes not create a nested Promise** βœ… **Explicitawait` before returning a Promise is unnecessary

This auto-unwrapping makes async functions easier to work with, avoiding extra .then() chains or Promise<Promise<T>> situations.

Did you find this behavior surprising? πŸ˜„

@tivrfoa

tivrfoa commented Feb 17, 2025

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment