26

I see there is an eslint rule, no-return-await, for disallowing return await.

In the rule's description, it states a return await adds "extra time before the overarching Promise resolves or rejects".

However, when I look at MDN async function docs, the "Simple Example" shows an example containing return await without any description of why this might be a performance problem.

Is return await an actual performance problem as the eslint docs suggest?

And if so, how?

sfletche
  • 47,248
  • 30
  • 103
  • 119
  • As far as I'm aware, `return` and `return await` should be functionally equivalent, so I can't see why any reasonable interpreter should not just optimize this in the first place. I don't think transpliers would do this optimization though (at least [Babel doesn't](https://babeljs.io/repl/#?babili=false&evaluate=false&lineWrap=false&presets=stage-2&targets=&browsers=&builtIns=false&code=async%20function%20foo()%20%7B%0A%20%20return%20Promise.resolve(123)%3B%0A%7D%0A%0Aasync%20function%20bar()%20%7B%0A%20%20return%20await%20Promise.resolve(123)%3B%0A%7D)) so it might make some difference there. – Frxstrem Apr 11 '17 at 18:04
  • 2
    See also [Difference between `return await promise` and `return promise`](https://stackoverflow.com/q/38708550/1048572) – Bergi May 30 '17 at 17:49
  • Possible duplicate of [Difference between \`return await promise\` and \`return promise\`](https://stackoverflow.com/questions/38708550/difference-between-return-await-promise-and-return-promise) – chharvey Oct 03 '18 at 20:13
  • 1
    Adding an `await` instead of returning a the promise of the sub-function gives you async. stack traces. Already available in V8 behind a flag. 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:41
  • @Mörre While I haven't tried out the flag myself yet, judging from the [detailed description of async stack traces](https://docs.google.com/document/d/13Sy_kBIJGP0XT34V1CV3nkWya4TwYx9L3Yv45LdGB6Q) (especially the "*Arbitrary Promise chains*" section), it still should be possible to reconstruct the function name. The implicit promise that is returned by the `async function` is still found in the promise reactions, and it is managed by the engine. So if the `return`ing function is skipped in the stack trace, I'd consider that a missing feature/bug in the engine, not a deficiency of the code. – Bergi Jun 22 '19 at 21:15
  • @Bergi One, it is not behind a flag any more in the latest V8, two, why don't you just try it (latest Chrome browser for example, use the example on the linked page slightly modified to return the promise with/without `await`)? No, `return` without `await` does not provide the stack trace (as expected, as far as I'm concerned). It does not matter where you or anyone sees the/a deficiency - what is is, and the fact is easy to see why collecting the stack trace is much cheaper and therefore actually doable in practice (for the runtime) with `return await`. – Mörre Jun 23 '19 at 08:41

2 Answers2

34

No, there isn't any performance problem. It's just an unnecessary extra operation. It might take a bit longer to execute, but should be hardly noticeable. It's akin to return x+0 instead of return x for an integer x. Or rather, exactly equivalent to the pointless .then(x => x).

It doesn't do actual harm, but I'd consider it bad style and a sign that the author does not fully compre­hend promises and async/await.

However, there's one case where it make an important difference:

try {
    …
    return await …;
} …

await does throw on rejections, and in any case awaits the promise resolution before catch or finally handlers are executed. A plain return would have ignored that.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    IMO comparing it to return `.then(x => x)` is a bit harsh. As you pointed out, it is necessary in a try/catch. It would be maybe more like `.then((x) => x.data)` vs `.then(x => x.data)`. I.E. in cases where you have multiple parameters, the parentheses around x are necessary, otherwise they are optional. I would view this as the same mindset with the debate being: Is it better to be consistent or to minimize? – TigerBear Aug 22 '18 at 14:43
  • @TigerBear No, it's not harsh - it's exactly what is happening there. Writing `return await …` would be odd, just like `await await …` would be odd. It works, but is useless. – Bergi Aug 22 '18 at 18:23
  • 1
    I wouldn't call it useless though since, as you already pointed out, it sometimes has a use, meaning it falls in a different category. Consistent vs Minimal. Similar to parentheses around arrow function parameters – TigerBear Aug 23 '18 at 14:56
  • @TigerBear That's a very different construct that I am not calling useless, and there is no reason to consistently use `return await` in the different cases. `await` in a `try` block is equivalent to `.then(x => x, err => …)` or `.catch(err => …)` and is obviously not pointless. – Bergi Aug 24 '18 at 08:16
  • 1
    _"I'd consider it bad style and a sign that the author does not fully compre­hend promises"_ -- Careful with that - you yourself are not aware of _zero-cost async stack traces_ enabled by the await (already in V8 right now). See my comment below the question with the link. Also, with runtimes that don't do that you could use try/catch to build your own full stack trace, potentially valuable for some portions of code (try finding the cause of a problem if the function raising it, e.g. "file not found", is called from many different places and the filename does not help - which function was it? – Mörre Jun 22 '19 at 19:37
  • 3
    FWIW, as of a change in ES2020, provided the promise being awaited (`x`) is a native one (not a 3rd party thenable), `return await x;` and `return x;` outside a `try`/`catch` are now handled exactly the same way. No extra async cycle. – T.J. Crowder Sep 01 '20 at 06:49
  • @T.J.Crowder do you have a reference for that change? – Denis Pshenov Sep 14 '21 at 17:28
  • @DenisPshenov - Not at hand, I'll see if I can dig it up tomorrow, it's somewhere in the ecma262 commit history from a couple of years back as a "normative change." But warning: My phrasing above is ***really*** bad. They aren't handled "exactly the same." It's just that there's no extra async tick added by `return await` anymore (in that case: a native promise outside a `try`). There are definitely differences (for instance, in the recorded stack trace of an error). – T.J. Crowder Sep 14 '21 at 21:21
  • 1
    @T.J.Crowder Wasn't that more like a "there's only a single extra tick instead of two extra ticks"? If you refer to [that change](https://github.com/tc39/ecma262/pull/1250) (also discussed in https://v8.dev/blog/fast-async)? – Bergi Sep 14 '21 at 21:26
  • @Bergi - Fairly sure that's the change I'm thinking of, yes. I'm not going to debate "extra," but the point is that one arguably-unnecessary one was (allowed to be) optimized out. IIRC, you can see the difference in Node v12 vs. Node v13. Don't have more specific version numbers handy, and it's late (for me :-) ). – T.J. Crowder Sep 14 '21 at 21:31
  • @T.J.Crowder Nah, something else is going on there. Testing with current chromium, `(async () => { return …; })().then(console.log)` (against a separate `.then()` chain to measure ticks). Results: `return 'a'` - 1 tick. `return await 'a'` - 2 ticks. `return await Promise.resolve('a')` - 2 ticks. `return Promise.resolve('a')` - 3 ticks?! – Bergi Sep 14 '21 at 21:44
  • @T.J.Crowder If you want to go spec-hunting, I can ask a question about that – Bergi Sep 14 '21 at 21:47
  • @Bergi - I don't have time at the moment to go looking for the details *(deadline, ugh)*, but [this code](https://jsfiddle.net/tjcrowder/879yqzum/) demonstrates the optimization I'm referring to. It removed one unnecessary tick from `return await `. (I thought it only applied if the `return await` wasn't in a `try` block, but a quick test this morning says I'm wrong about that, so... Looks like I remembered correctly about it only handling this for native promises at least; https://jsfiddle.net/tjcrowder/67esarm8/ [for old behavior, that's one's most easily run in Node]). – T.J. Crowder Sep 15 '21 at 07:15
  • I think I figured it out guys. The explanation is a bit long, so I added another answer. – ShortFuse Feb 03 '22 at 22:33
  • 1
    @T.J.Crowder https://github.com/tc39/ecma262/issues/2770 :-) – Bergi May 07 '22 at 16:48
  • *a sign that the author does not fully compre­hend promises* this is a terrible argument, given that even the greatest minds here have pages long of debate about the topic, it sounds like you are making stuff harder just to show off that we comprehend promises better. Have you tried to explain to a peer: always await an async function call, except when it is a return, except when there is a try-catch. They said it's ridiculous, and they were right IMO. All this to save a microtask? And ShortFuse just showed that it is actually slower than awaiting. – Tamas Hegedus Jul 14 '22 at 10:23
17

I'm adding an answer because a comment would be too long. I originally had a very long, verbose explanation about how async works and await work. But it's just so convoluted that actual data may just be easier to understand. So here is the, uh, simplified explanation. Note: This is run on Chrome v97, FireFox v95, and Node v16 with the same results.

The answer as to what's faster: it depends on what you're returning and how you're calling it. await works differently than async because it runs PromiseResolve (similar to Promise.resolve but it's internal). Basically, if you run await on a Promise (a real one, not a polyfill), await doesn't try to wrap it. It executes the promise as is. That skips a tick. This is a "newer" change from 2018. In summary, evaluating await always returns the result of a Promise, not a Promise, while avoiding wrapping Promises when possible. That means await always takes at least one tick.

But that's await and async doesn't actually use this bypass. Now async uses the good ol' PromiseCapability Record. We care about how this resolves promises. The key points are it'll instantly start fulfilling if the resolution is "not an Object" or if .then is not Callable. If neither are true, (you're returning a Promise), it'll perform a HostMakeJobCallback and tack on to the then in the Promise, which basically means, we're adding a tick. Clarified, if you return a Promise in an async function, it'll add on an extra tick, but not if you return a Non-Promise.

So, with all that preface (and this is the simplified version), here's your chart as to how many ticks until your await foo() call is returned:

Non-Promise Promise
() => result 1 1
async () => result 1 3
async () => await result 2 2

This is tested with await foo(). You can also test with foo().then(...), but the ticks are the same. (If you don't use an await, then the sync function would indeed be 0. Though foo().then would crash, so we need something real to test with.) That means our floor is 1.

If you understood my explanations above (hopefully), this will make sense. The sync function makes sense because at no point in the function do we call for paused execution: await foo() will take 1 tick.

async likes Non-Promises and expects them. It will return immediately if it finds one. But if it finds a Promise, it'll tack on to that Promise's then. That means it'll execute the Promise (+1) and then wait for the then to complete (another +1). That's why it's 3 ticks.

await will convert a Promise to a Non-Promise which is perfect for async. If you call await on a Promise, it'll execute it without tacking any extra ticks (+1). But, await will convert a Non-Promise into a Promise and then run it. That means await always takes a tick, regardless of what you call it against.

So, in conclusion, if you want the fastest execution, you want to make sure your async function always includes at least one await. If it doesn't, then just make it synchronous. You can always call await on any synchronous function. Now, if you want to really tweak performance, and you are going to use async, you have to make sure always return a Non-Promise, not a Promise. If you are returning a Promise, convert it first with await. That said you can mix and match like this:


async function getData(id) {
  const cache = myCacheMap.get(id);
  if (cache) return cache; // NonPromise returns immediately (1 tick)
  
  // return fetch(id); // Bad: Promise returned in async (3 ticks)
  return await fetch(id); // Good: Promise to NonPromise via await (2 ticks)
}

With that in mind, I have a bunch of code to rewrite :)


References:

https://v8.dev/blog/fast-async

https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-async-functions-abstract-operations-async-function-start


Test:

async function test(name, fn) {
  let tick = 0;
  const tock = () => tick++;
  Promise.resolve().then(tock).then(tock).then(tock);

  const p = await fn();
  console.assert(p === 42);
  console.log(name, tick);
}

await Promise.all([
  test('nonpromise-sync', () => 42),
  test('nonpromise-async', async () => 42),
  test('nonpromise-async-await', async () => await 42),
  test('promise-sync', () => Promise.resolve(42)),
  test('promise-async', async () => Promise.resolve(42)),
  test('promise-async-await', async () => await Promise.resolve(42)),
]);

setTimeout(() => {}, 100);
ShortFuse
  • 5,970
  • 3
  • 36
  • 36