Around IT in 256 seconds

Is 'return await' redundant or necessary in JavaScript?

August 05, 2024 | 3 Minute Read

In JavaScript, should we await on a returned promise or not?

async function foo() {
    //...
    return await waitAndMaybeReject();
}

If we remove await, the code compiles and behaves exactly the same way. Almost.

TL;DR:

Always add await and gain better async stack traces.

Longer version

I went through the rabbit hole of return followed by await or not. Until today I thought it was redundant. Now I know it’s crucial. I thought it’s complicated, but actually it’s not. Although there are many (wrong) articles claiming await is unnecessary, it actually is very valuable.

For starters, await is definitely needed when wrapped within try-catch:

async function foo() {
    try {
        return await waitAndMaybeReject();
 } catch(error) {
        console.error(error);
 }
}

Without await, errors thrown from waitAndMaybeReject() will not be captured by catch statement. But that’s the basics. Aside from error checking, return followed by await seemed redundant. There even used to be an eslint rule no-return-await that suggested removing redundant return await instead of simple await. However, the rule became deprecated, as explained in Remove no-return-await rule #12246, as well as in Documentation of “no-return-await” #11878. This rule was also removed from JavaScript Standard Style guide.

There’s one big reason to keep await - a more descriptive stack trace when promise got rejected. Example taken from this ticket:

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

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

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

This yields proper stack trace (tested on Node 21.6.2):

> Error: BEEP BEEP
 at bar (REPL13:9:11)
 at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
 at async foo (REPL13:4:12)

However, if we remove await from foo() function:

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

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

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

The stack trace no longer shows foo, which is pretty bad:

> Error: BEEP BEEP
 at bar (REPL26:8:11)
 at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

It turns out that improved stack traces were introduced in V8 engine version 7.3 (since Node 12) with --async-stack-traces option. The only reason I was semi-automatically removing await was WebStorm suggesting that by default and obsolete eslint rule. But these days the recommendation is the opposite. Always keep await and enjoy a better troubleshooting experience of asynchronous code.

But performance!

There also used to be a tiny performance penalty of including await. It had something to do with one extra microtask on the event loop. However, this optimization is no longer valid, and probably even counterproductive.

Tags: JavaScript

Be the first to listen to new episodes!

To get exclusive content: