73

What is the difference between

return await foo()

and

const t = await foo();
return t

http://eslint.org/docs/rules/no-return-await

AturSams
  • 7,568
  • 18
  • 64
  • 98
  • 9
    You really should not do the 2nd version as it adds no benefit. The correct code line would be `return foo();` – Igor Jun 28 '17 at 15:02
  • @Igor Both code samples would be equally redundant, just written slightly different. `return foo();` would be correct though. – samanime Jun 28 '17 at 15:09
  • 2
    So why doesn't it not state `const x = await foo; return x` should be avoided? – AturSams Jun 28 '17 at 15:12
  • 1
    @zehelvion - probably because detection of that is difficult to do at transpile time but both statements are equivalent and should be avoided. – Igor Jun 28 '17 at 15:13
  • 1
    Have a look at [this explanation of the `no-return-await`](https://stackoverflow.com/q/43353087/1048572) rule – Bergi Jul 20 '17 at 09:20
  • 2
    @Igor The advantage of `return await` is that - already in V8 behind a flag - you get a full asynchronous stacktrace. That's because the full stack still is easily reconstructable as long as the original function did not yet finish. Without the `await`, if you return the promise directly, the function would be gone for good when the actual promise-creating function deeper in the call stack throws. Look for "**zero-cost async stack traces**" on https://v8.dev/blog/fast-async – Mörre Jun 22 '19 at 19:45
  • FYI eslint `no-return-await` is now **deprecated**. Sonarcloud also deprecated their similar rule. There are long threads about why, the consensus seems to be that introducing this rule was a mistake. – Tamas Hegedus Aug 10 '23 at 10:25

6 Answers6

91

Basically, because return await is redundant.

Look at it from a slightly higher level of how you actually use an async function:

const myFunc = async () => {
  return await doSomething();
};

await myFunc();

Any async function is already going to return a Promise, and must be dealt with as a Promise (either directly as a Promise, or by also await-ing).

If you await inside of the function, it's redundant because the function outside will also await it in some way, so there is no reason to not just send the Promise along and let the outer thing deal with it.

It's not syntactically wrong or incorrect and it generally won't cause issues. It's just entirely redundant which is why the linter triggers on it.

samanime
  • 25,408
  • 15
  • 90
  • 139
  • 2
    Im not sure 'send the promise along and let the outer thing deal with it' is 100% correct. In my testing, a new promise is created by both returning the promise directly, and awaiting then returning the value: `function waitForN(time) { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('Rejected')); }, time); }); } async function testing() { myPromise = waitForN(1000); myPromise.catch(error => console.log(error)); return myPromise; }` This will result in one caught and one uncaught promise. – TigerBear Aug 20 '18 at 15:02
  • @TigerBear It is actually your call to `myPromise.catch()` that causes it to make a new `Promise`. Has nothing to do with `async/await`. Calling `.catch()` or `.then()` creates a new `Promise`, which is why you normally chain them and return the result of the whole thing. You should be doing `return myPromise.catch()`. – samanime Aug 20 '18 at 15:55
  • @samanime true, this creates a promise to allow chaining, but I am not returning the newly created promise, and am returning `myPromise`. You can call my above code snippet with this: `test = testing(); test.catch(e=> console.log(e))` and now both will be caught. So I am creating a promise, but it is not the one that I am returning. This created promise is ignored and shouldn't have any affect. I am already catching the one I am returning, but when calling my function, I still need to catch the promise created and returned by the async. Does that make sense? Am I overlooking something? – TigerBear Aug 21 '18 at 07:01
  • If you just return the chained, it'll all be caught. You're essentially forking the chain, which creates two different paths, which is different than your second example. If you wanted to open up a question asking about that specific scenario, could probably get some pretty good answers that go more in-depth. – samanime Aug 21 '18 at 17:12
  • 1
    @samanime See my comment under OPs question. Since the introduction of "zero-cost async stack traces" in V8 `return await` is significantly different from returning the promise: You get a stack trace (in V8 at least, as of now)! – Mörre Jun 22 '19 at 19:47
  • @Mörre I read through the article. Interesting stuff. Looks like it is enabled by default now. I did some testing comparing two functions: `() => p()` and `() => await p()` and calling those with `await()`. It doesn't look like there is any appreciable difference between the two. If `p = () => { throw new Error() }` you get a nice stack trace. If `p = () => Promise.reject()` you get something ugly. However, it seems like whether it is called `p()` or `await p()` gives the same result either way. – samanime Jun 23 '19 at 10:54
  • 1
    @samanime Not in my testing. I get a stack trace. Which is exactly what they claim.If you don't get what they claim you should file a report. Also beware of too simplistic tests, promises and their "microtask" queues based implementation don't behave the same if it is not truly asynchronous. – Mörre Jun 23 '19 at 13:51
  • @Mörre I get stack traces too, as long as `p = () => { throw new Error() }`. However, it doesn't seem to matter if the calling function of `p` does `() => p()` or `() => await p()`. – samanime Jun 23 '19 at 18:54
  • 1
    @samanime I refer to my previous comment... whatever you are testing, it is not that functionality. It works, I tried - Google tried. Or if not than in your setup there is a bug. Find it or report it. – Mörre Jun 23 '19 at 19:09
  • 1
    with `try{ return await foo() } finally { /* some code */ }` it makes sense though. Removing the `await` change the semantic to "after getting the promise, do this whatever happens" instead of the intended "after the promise settle (resolve or reject) , do this". It's quite easy to overlook such construct. – programaths Jun 10 '21 at 08:31
57

Using return await does have some newly introduced benefits in v8 engine used by Node.js, Chrome and a few other browsers:

v8 introduced a --async-stack-traces flag which as of V8 v7.3 is enabled by default (Node.js v12.0.0).

This flags provides an improved developer experience by enriching the Error stack property with async function calls stack trace.

async function foo() {
  return bar();
}

async function bar() {
  await Promise.resolve();
  throw new Error('BEEP BEEP');
}

foo().catch(error => console.log(error.stack));


Error: BEEP BEEP
    at bar (<anonymous>:7:9)

Note that by calling return bar(); foo() function call does not appear at all in the error stack. Changing it to return await bar(); gives a much better error stack output:

async function foo() {
  return await bar();
}
foo();

Error: BEEP BEEP
    at bar (<anonymous>:7:9)
    at async foo (<anonymous>:2:10)

This indeed does provide much better error stack tracing, hence it is HIGHLY encouraged to always await your promises.

Additionally, async/wait now outperforms hand written promises:

async/await outperforms hand-written promise code now. The key takeaway here is that we significantly reduced the overhead of async functions — not just in V8, but across all JavaScript engines, by patching the spec. Source

Read more about these changes on the v8.dev blog: https://v8.dev/blog/fast-async#improved-developer-experience

Kostas Minaidis
  • 4,681
  • 3
  • 17
  • 25
Bamieh
  • 10,358
  • 4
  • 31
  • 52
  • 2
    This doesn't seem to explain why `const x = await foo()` is better than `return await foo()` if that is the case? – AturSams Jun 28 '17 at 15:09
  • 2
    Well, if all you do is const x = and then return it will be able to detect it but I get the idea. It is exactly the same and the reason it doesn't warn against it, is that it doesn't want to bother with detecting the more complicated use case. – AturSams Jun 29 '17 at 10:09
  • 6
    *"Inside an async function, return await is useless"* -- not if the `return` statement is inside a `try ... catch` block. In that case, awaiting will allow you to handle any errors in place. – John Weisz Aug 24 '17 at 11:17
  • @JohnWeisz true *with caution*. You can also add a `.catch` to avoid the extra wrapping though – Bamieh Mar 21 '18 at 15:04
  • 3
    "return await" is VERY useful if you run code on a current version of V8 (e.g. current node.js) - waiting before returning lets V8 compose full async. stack traces (at no "cost", because the context of the calling function is available "for free" because of the `await`). – Mörre Jul 14 '19 at 08:09
  • @Mörre interesting, can you provide a source for this? – Bamieh Jul 14 '19 at 10:11
  • 2
    @Bamieh https://v8.dev/blog/fast-async -- scroll down to "zero-cost"; Note that the feature is enabled by default now, e.g. in current node.js and Chrome versions. – Mörre Jul 16 '19 at 11:14
  • Oh thank you so much for sharing this. I'll update the answer! – Bamieh Jul 16 '19 at 15:39
  • Is `return await` still relevant as of 2022? – Ginden Feb 24 '22 at 12:15
  • @Ginden Yes. running this in the browser's console will reveal that return await is still needed to reveal the correct stack trace – Bamieh Mar 02 '22 at 15:07
  • Interestingly, this is not a case in Firefox, which puts foo on stack trace in both cases. – František Žiačik Aug 09 '23 at 12:56
24

Because you can just

async function() {
  return foo();
}

The returned result of async function always is Promise, no matter you return the exact value or another Promise object inside the function body

edvard chen
  • 2,320
  • 1
  • 12
  • 10
  • 3
    This is the best answer here!! – Yoni Rabinovitch Aug 01 '18 at 08:24
  • instead of `return await foo()`, just `return foo()`. – jmazin Mar 13 '19 at 16:19
  • 3
    @YoniRabinovitch Not any more. "return await" is VERY useful if you run code on a current version of V8 (e.g. current node.js) - waiting before returning lets V8 compose full async. stack traces (at no "cost", because the context of the calling function is available "for free" because of the `await`). – Mörre Jul 14 '19 at 08:09
  • But if you later wrap the return in a try-catch, you must remember to add the await. By skipping await you are sacrificing correctness and refactorability. – Tamas Hegedus Aug 10 '23 at 10:34
6

Update: The no-return-await rule is now deprecated. The world is healing.

It seems many are debating the usefulness of return await in the comments. Let me add a demonstration that shows how the stacktrace is affected.

In my opinion, skipping await is a misguided attempt at some sense of "optimization", saving 6 characters and a microtask (EDIT: ShortFuse just showed that it is actually not saving but adding a microtask in current v8) at the cost of stacktraces, try-catches, refactorability and code consistency. The rule of the thumb is simple: if you have an async call, await it (unless you are doing fancy stuff like parallelism or async caching). I believe we should stick to the rule of thumb and always use return await, and switch off that no-return-await eslint rule.

little demo:

async function main() {
  console.log("\nStatcktrace with await shows testStackWithAwait:");
  await testStackWithAwait().catch(logStack);
  console.log("\nStatcktrace without await hides testStackWithoutAwait:");
  await testStackWithoutAwait().catch(logStack);
  console.log("\nFinally happens before try block ends without await:");
  await testFinallyWithoutAwait();
}

async function fnThatThrows() {
  await delay(1);
  throw new Error();
}

async function testStackWithoutAwait() {
  return fnThatThrows(); // bad
}

async function testStackWithAwait() {
  return await fnThatThrows(); // good
}

async function fnThatLogs() {
  await delay(1);
  console.log('inside');
}

async function testFinallyWithoutAwait() {
  try {
    return fnThatLogs(); // bad
  } finally {
    console.log('finally');
  }
}

function logStack(e) {
  console.log(e.stack);
}

function delay(timeout, value) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(value);
    }, timeout);
  });
}

main().catch(console.error);

On Chrome 103 on Windows I get the following logs:

Statcktrace with await shows testStackWithAwait:
Error
    at fnThatThrows (https://stacksnippets.net/js:23:9)
    at async testStackWithAwait (https://stacksnippets.net/js:31:10)
    at async main (https://stacksnippets.net/js:14:3)

Statcktrace without await hides testStackWithoutAwait:
Error
    at fnThatThrows (https://stacksnippets.net/js:23:9)
    at async main (https://stacksnippets.net/js:16:3)

Finally happens before try block ends without await:
finally
inside
Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
3

The significant difference between return asyncFunc and return await Promise.resolve() is by following the second approach you can catch an error if something wrong inside async function.

function afunction() {
  return asyncFun();
}
   
// with await
async function afunction() {
  try {
    return await asyncFun();
  } catch(err) {
    handleError(err);
    // return error result;
  }
}

   
    
Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
-1

Oh I think is easy to understand, we put "await" when we wait for a specific value in order to continue a process, if the process is finished (look at the return), we don't need the await syntax anymore so.

return await foo(); //is redundant
return foo(); //is the correct way