216

Given the code samples below, is there any difference in behavior, and, if so, what are those differences?

return await promise

async function delay1Second() {
  return (await delay(1000));
}

return promise

async function delay1Second() {
  return delay(1000);
}

As I understand it, the first would have error-handling within the async function, and errors would bubble out of the async function's Promise. However, the second would require one less tick. Is this correct?

This snippet is just a common function to return a Promise for reference.

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
PitaJ
  • 12,969
  • 6
  • 36
  • 55
  • Why did you change your question?? That makes my answer "weird" and hard to understand. Please don't do that. – Amit Aug 01 '16 at 21:48
  • @Amit yeah I just rearranged things. Sry bout that. – PitaJ Aug 01 '16 at 21:48
  • I deleted my answer since it doesn't make sense once you change your again (again). Please avoid doing this in the future, it's a bad habit. – Amit Aug 01 '16 at 21:52
  • 3
    Yeah I edited my question because you misunderstood my meaning and it didn't really answer what I was wondering. – PitaJ Aug 01 '16 at 21:55
  • 1
    @PitaJ: I believe you meant to remove the `async` from your second (`return promise`) sample. – Stephen Cleary Aug 01 '16 at 22:49
  • 1
    @StephenCleary nope. I meant for this. Imagine there are other await calls, etc before the return. – PitaJ Aug 01 '16 at 22:53
  • 1
    @PitaJ: In that case, your second example would return a promise that is resolved with a promise. Rather odd. – Stephen Cleary Aug 02 '16 at 00:53
  • 7
    https://jakearchibald.com/2017/await-vs-return-vs-return-await/ is a nice article that summarises the differences – sanchit Jan 03 '18 at 06:34
  • [eslint](https://github.com/eslint/eslint/blob/master/docs/rules/no-return-await.md) shows that return await is unnecessary and should avoid using that. – foxiris Nov 29 '18 at 09:58
  • 2
    @StephenCleary, I stumbled upon this and first thought exactly the same, a promise that is resolved with a promise doesn't make sense here. But as it turns, `promise.then(() => nestedPromise)` would flatten and "follow" the `nestedPromise`. Interesting how it's different from nested tasks in C# where we'd have to `Unwrap` it. On a side note, [it appears that](https://stackoverflow.com/q/53146565/1768303) `await somePromise` calls `Promise.resolve(somePromise).then`, rather than just `somePromise.then`, with some interesting semantic differences. – noseratio Dec 10 '18 at 10:31
  • https://hassansin.github.io/Why-return-await-Is-a-Bad-Idea I've found a nice experiment, which logs why you should prefer `return Promise` instead of `return await Promise`. TL;DR: should not waste CPU cycles, you await only when you need to process or act (error handling) on the returned data. – Aldekein Jun 23 '21 at 11:41

7 Answers7

293

Most of the time, there is no observable difference between return and return await. Both versions of delay1Second have the exact same observable behavior (but depending on the implementation, the return await version might use slightly more memory because an intermediate Promise object might be created).

However, as @PitaJ pointed out, there is one case where there is a difference: if the return or return await is nested in a try-catch block. Consider this example

async function rejectionWithReturnAwait () {
  try {
    return await Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

async function rejectionWithReturn () {
  try {
    return Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

In the first version, the async function awaits the rejected promise before returning its result, which causes the rejection to be turned into an exception and the catch clause to be reached; the function will thus return a promise resolving to the string "Saved!".

The second version of the function, however, does return the rejected promise directly without awaiting it within the async function, which means that the catch case is not called and the caller gets the rejection instead.

Denis Washington
  • 5,164
  • 1
  • 18
  • 21
  • 2
    Maybe also mention that the stack trace would be different (even without a try/catch)? I think that's the issue people run into the most often in this example :] – Benjamin Gruenbaum Apr 26 '19 at 19:19
  • i have found in one scenario, that using `return new Promise(function(resolve, reject) { })` within a `for...of` loop and then calling `resolve()` within the loop after a `pipe()` does not pause program execution till pipe has completed, as desired, however using `await new Promise(...)` does. is the latter even valid/correct syntax? is it ‘shorthand’ for `return await new Promise(...)`? could you help me understand why the latter works and the former does not? for context, the scenario is in `solution 02` of [this answer](https://stackoverflow.com/a/57346767) – user1063287 Aug 15 '19 at 00:39
  • Presumably this also holds for `finally` blocks? – Ben Aston Aug 18 '22 at 12:48
  • Finally blocks will still run, just out of order. Those will run before the promise is finished. – Tamas Hegedus Nov 10 '22 at 17:50
29

As other answers mentioned, there is likely a slight performance benefit when letting the promise bubble up by returning it directly — simply because you don’t have to await the result first and then wrap it with another promise again. However, no one has talked about tail call optimization yet.

Tail call optimization, or “proper tail calls”, is a technique that the interpreter uses to optimize the call stack. Currently, not many runtimes support it yet — even though it’s technically part of the ES6 Standard — but it’s possible support might be added in the future, so you can prepare for that by writing good code in the present.

In a nutshell, TCO (or PTC) optimizes the call stack by not opening a new frame for a function that is directly returned by another function. Instead, it reuses the same frame.

async function delay1Second() {
  return delay(1000);
}

Since delay() is directly returned by delay1Second(), runtimes supporting PTC will first open a frame for delay1Second() (the outer function), but then instead of opening another frame for delay() (the inner function), it will just reuse the same frame that was opened for the outer function. This optimizes the stack because it can prevent a stack overflow (hehe) with very large recursive functions, e.g., fibonacci(5e+25). Essentially it becomes a loop, which is much faster.

PTC is only enabled when the inner function is directly returned. It’s not used when the result of the function is altered before it is returned, for example, if you had return (delay(1000) || null), or return await delay(1000).

But like I said, most runtimes and browsers don’t support PTC yet, so it probably doesn’t make a huge difference now, but it couldn’t hurt to future-proof your code.

Read more in this question: Node.js: Are there optimizations for tail calls in async functions?

chharvey
  • 8,580
  • 9
  • 56
  • 95
  • The `await` actually offers more protection against stack overflows in typical use: if a recursive async function awaits anything other than itself, it will return immediately meaning its stack frame won't stick around. The named values that were on the stack are in fact preserved in the same way that a closure would be (in fact, that is the runtime implementation, meaning it's on the heap). Consider that an async function that awaits only itself and no actual I/O or timeouts doesn't need to be async, so it's more or less useless, so typically the stack overflow is *less* of a concern – Mack Jul 16 '23 at 14:13
26

Noticeable difference: Promise rejection gets handled at different places

  • return somePromise will pass somePromise to the call site, and await somePromise to settle at call site (if there is any). Therefore, if somePromise is rejected, it will not be handled by the local catch block, but the call site's catch block.

async function foo () {
  try {
    return Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'OUT'
  • return await somePromise will first await somePromise to settle locally. Therefore, the value or Exception will first be handled locally. => Local catch block will be executed if somePromise is rejected.

async function foo () {
  try {
    return await Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'IN'

Reason: return await Promise awaits both locally and outside, return Promise awaits only outside

Detailed Steps:

return Promise

async function delay1Second() {
  return delay(1000);
}
  1. call delay1Second();
const result = await delay1Second();
  1. Inside delay1Second(), function delay(1000) returns a promise immediately with [[PromiseStatus]]: 'pending. Let's call it delayPromise.
async function delay1Second() {
  return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. Async functions will wrap their return value inside Promise.resolve()(Source). Because delay1Second is an async function, we have:
const result = await Promise.resolve(delayPromise); 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. Promise.resolve(delayPromise) returns delayPromise without doing anything because the input is already a promise (see MDN Promise.resolve):
const result = await delayPromise; 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. await waits until the delayPromise is settled.
  • IF delayPromise is fulfilled with PromiseValue=1:
const result = 1; 
  • ELSE is delayPromise is rejected:
// jump to catch block if there is any

return await Promise

async function delay1Second() {
  return await delay(1000);
}
  1. call delay1Second();
const result = await delay1Second();
  1. Inside delay1Second(), function delay(1000) returns a promise immediately with [[PromiseStatus]]: 'pending. Let's call it delayPromise.
async function delay1Second() {
  return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. Local await will wait until delayPromise gets settled.
  • Case 1: delayPromise is fulfilled with PromiseValue=1:
async function delay1Second() {
  return 1;
}
const result = await Promise.resolve(1); // let's call it "newPromise"
const result = await newPromise; 
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: 1
const result = 1; 
  • Case 2: delayPromise is rejected:
// jump to catch block inside `delay1Second` if there is any
// let's say a value -1 is returned in the end
const result = await Promise.resolve(-1); // call it newPromise
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: -1
const result = -1;

Glossary:

  • Settle: Promise.[[PromiseStatus]] changes from pending to resolved or rejected
Ragtime
  • 382
  • 3
  • 6
  • Explained beautifully! The step by step wrapping and unwrapping of promises made the difference crystal clear. One of the important takeaways from this is the value returned by Promise.resolve when a promise is passed. I had initially thought that it would return a resolved promise but no, it returns back the promise as is. – Jamāl Jun 10 '21 at 12:40
  • There's also a performance difference, though likely negligible, making `return await` faster. See https://stackoverflow.com/questions/43353087/are-there-performance-concerns-with-return-await/70979225#70979225 Coupled with better error handling, you should probably always use `return await` and pass up errors if you want to expose them. – ShortFuse Jul 05 '23 at 21:07
5

This is a hard question to answer, because it depends in practice on how your transpiler (probably babel) actually renders async/await. The things that are clear regardless:

  • Both implementations should behave the same, though the first implementation may have one less Promise in the chain.

  • Especially if you drop the unnecessary await, the second version would not require any extra code from the transpiler, while the first one does.

So from a code performance and debugging perspective, the second version is preferable, though only very slightly so, while the first version has a slight legibility benefit, in that it clearly indicates that it returns a promise.

nrabinowitz
  • 55,314
  • 10
  • 149
  • 165
  • Why would the functions behave the same? The first returns a resolved value (`undefined`) and the second returns a `Promise`. – Amit Aug 01 '16 at 21:54
  • 4
    @Amit both functions return a Promise – PitaJ Aug 01 '16 at 21:55
  • Ack. This is why I can't stand `async/await` - I find it much harder to reason about. @PitaJ is correct, both functions return a Promise. – nrabinowitz Aug 01 '16 at 21:57
  • What if I were to surround the body of both async functions with a `try-catch`? In the `return promise` case, any `rejection` would not be caught, correct, whereas, in the`return await promise` case, it would be, right? – PitaJ Aug 01 '16 at 22:00
  • Both return a Promise, but the first "promises" a primitive value, and the second "promises" a Promise. If you `await` each of these at some call site, the result will be very different. – Amit Aug 01 '16 at 22:00
  • @Amit the result will be exactly the same. If the delay function were to `resolve` to a value, that value would be returned into an async function awaiting `delay1Second` regardless of which `delay1Second` you use. – PitaJ Aug 01 '16 at 22:04
  • @PitaJ - suppose you're right, and suppose I want to create an async function that returns a Promise (not the value wrapped in the Promise, the Promise itself) so that I could await that **later**. What would that function look like?? – Amit Aug 01 '16 at 22:07
  • You'd just not `await` it in the `async` function until later. – PitaJ Aug 01 '16 at 22:08
  • @PitaJ (for example, a function that fetches a delay length value from a service asynchronously, and then returns a delay promise with that value as the delay period, but I want the "timer to start ticking" before the function returns) – Amit Aug 01 '16 at 22:09
  • @Amit like this: `async function outer() { let x = delay(); /* ... */ await x; }` – PitaJ Aug 01 '16 at 22:10
  • @PitaJ not good enough... That means `delay` didn't get a chance to complete it's task and actually trigger the delay before returning – Amit Aug 01 '16 at 22:12
  • Sorry, `await x` not `await delay` – PitaJ Aug 01 '16 at 22:12
  • If you want to insure that, then delay would have to Promise a function. Then you'd do: `async function outer() { let delay = await getDelay(); /* ... */ await delay(); }` – PitaJ Aug 01 '16 at 22:13
  • Or simply return the promise, as I explained. Try it out. `let x = await delay(); await x; async function delay(){ let t = await Promise.resolve(1000); return new Promise(done => setTimeout(done, t));}` – Amit Aug 01 '16 at 22:14
  • The second await won't do anything. It will just be awaiting undefined. The `let x = await delay()` will block the outer async function until `done` is called by the timer. Since nothing it passed to `done`, `x === undefined` – PitaJ Aug 01 '16 at 22:46
5

In our project, we decided to always use 'return await'. The argument is that "the risk of forgetting to add the 'await' when later on a try-catch block is put around the return expression justifies having the redundant 'await' now."

Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
MickH
  • 161
  • 2
  • 5
  • 2
    I 100% agree. Also explaining to new joiners that *always use await when calling async functions, except when it is immediately returned, except when it is in a try-catch* is just ridiculous. – Tamas Hegedus Jul 14 '22 at 10:41
1

Here is a typescript example that you can run and convince yourself that you need that "return await"

async function  test() {
    try {
        return await throwErr();  // this is correct
        // return  throwErr();  // this will prevent inner catch to ever to be reached
    }
    catch (err) {
        console.log("inner catch is reached")
        return
    }
}

const throwErr = async  () => {
    throw("Fake error")
}


void test().then(() => {
    console.log("done")
}).catch(e => {
    console.log("outer catch is reached")
});
David Dehghan
  • 22,159
  • 10
  • 107
  • 95
0

here i leave some code practical for you can undertand it the diferrence

 let x = async function () {
  return new Promise((res, rej) => {
    setTimeout(async function () {
      console.log("finished 1");
      return await new Promise((resolve, reject) => { // delete the return and you will see the difference
        setTimeout(function () {
          resolve("woo2");
          console.log("finished 2");
        }, 5000);
      });
      res("woo1");
    }, 3000);
  });
};

(async function () {
  var counter = 0;
  const a = setInterval(function () { // counter for every second, this is just to see the precision and understand the code
    if (counter == 7) {
      clearInterval(a);
    }

    console.log(counter);
    counter = counter + 1;
  }, 1000);
  console.time("time1");
  console.log("hello i starting first of all");
  await x();
  console.log("more code...");
  console.timeEnd("time1");
})();

the function "x" just is a function async than it have other fucn if will delete the return it print "more code..."

the variable x is just an asynchronous function that in turn has another asynchronous function, in the main of the code we invoke a wait to call the function of the variable x, when it completes it follows the sequence of the code, that would be normal for "async / await ", but inside the x function there is another asynchronous function, and this returns a promise or returns a" promise "it will stay inside the x function, forgetting the main code, that is, it will not print the" console.log ("more code .. "), on the other hand if we put" await "it will wait for every function that completes and finally follows the normal sequence of the main code.

below the "console.log (" finished 1 "delete the" return ", you will see the behavior.

  • 1
    While this code may solve the question, [including an explanation](https://meta.stackexchange.com/q/114762) of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please [edit] your answer to add explanations and give an indication of what limitations and assumptions apply. – Brian61354270 Apr 11 '20 at 17:23