iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article

3 Differences Between return and return await

に公開
6

When handling asynchronous functions that return a Promise, there are two ways to write them: returning the Promise as is, or returning the Promise after awaiting it.

const fetchUsers1 = async () => {
  return axios.get("/users");
};

// Or

const fetchUsers2 = async () => {
  return await axios.get("/users");
};

At first glance, the code examples above seem to behave identically, but there are differences such as:

  • Where exceptions are caught
  • Stack trace output
  • Microtask timing

Where exceptions are caught

The first and most notable example to consider is when they are used with a try/catch block. Briefly explained, in the case of return, the exception is caught by the caller of the function, whereas in the case of return await, the exception is caught inside the function.

Let's verify this with the following example. The alwaysRejectPromise function always rejects the Promise.

const func1 = async () => {
  try {
    return alwaysRejectPromise();
  } catch (e) {
    console.log("Caught inside func1");
  }
};

const func2 = async () => {
  try {
    return await alwaysRejectPromise();
  } catch (e) {
    console.log("Caught inside func2");
  }
};

func1().catch((e) => console.log("Caught at func1's caller"));
func2().catch((e) => console.log("Caught at func2's caller"));

The execution results are as follows:

Caught inside func2
Caught at func1's caller

As expected, in the case of func1, which simply returns the Promise, the exception is captured in the catch clause of the function's caller. In the case of func2, which uses return await, the exception is captured in the catch clause inside func2.

Why does this difference occur? The key point is that the await operator waits until the Promise is settled (fulfilled or rejected).

In other words, in the case of return await, since the function waits for the Promise to complete, the processing is still within the function when the Promise is rejected. Therefore, the exception is captured by the catch clause inside the function.

On the other hand, in the case of return, the function does not wait for the Promise to complete. Thus, by the time the Promise is rejected, the processing inside the function has already finished. Therefore, the exception is captured by the caller's catch clause instead of the internal one.

Stack trace output

The second difference is the difference in stack trace output. This follows the same principle as the location where the exception is caught, as mentioned earlier. Let's verify this with a concrete code example. First, here is an example using return.

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

async function bar() {
  await Promise.resolve();
  throw new Error("something went wrong");
}

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

The stack trace is output as follows:

Error: something went wrong
    at bar (index.js:7:9)

Next, we modify this to use return await inside the foo() function.

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

  async function bar() {
    await Promise.resolve();
    throw new Error("something went wrong");
  }

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

The stack trace details become more comprehensive compared to the previous version.

Error: something went wrong
    at bar (index.js:7:9)
    at async foo (index.js:2:10)

Microtask timing

Simply put, return await is redundant. In the following code, the function waits for the Promise to complete internally, and then the caller of the function must wait for the Promise to complete once more.

const foo = async () => {
  return await Promise.resolve(42);
};

const main = async () => {
  await foo();
  console.log("function foo finished");
};

main();

To understand how this becomes a problem, you need to understand the concept of microtasks in JavaScript.

What is a microtask?

Briefly stated, a microtask is a type of queue for asynchronous processing. For example, it is used in Promise and the Mutation Observer API. Microtasks follow a first-in, first-out (FIFO) queue; when a Promise is called, a task is registered in the microtask queue. Then, when the timing is right for microtasks to execute, the tasks are called and executed sequentially from the queue.

The timing when microtasks can execute is when one event loop cycle ends. After all tasks collected by the event loop have been completed, execution of the pending microtasks begins. In simpler terms, asynchronous processing (microtasks) is executed only after all synchronous processing has finished. Let's verify this with the following example:

console.log(1); // ①
console.log(2); // ②

Promise.resolve().then(() => console.log(3)); // ③

for (let i = 0; i < 1000000000; i++) {} // ④

console.log(4); // ⑤

First, processing ① and ② are executed following the order of the code. Then, in processing ③, a task is registered in the microtask queue and is not yet executed at this point. What's interesting is processing ④, where a massive for loop blocks the processing (let's roughly assume it takes 3000ms here). Although the execution of processing ③ actually completes in 1ms, tasks registered in the microtask queue must wait for all tasks in the current event loop to finish. After 3000ms has passed, processing ⑤ finally executes, and then the execution of microtasks begins, so 3 is output at the very end.

microtask1

Visualizing microtasks

As mentioned earlier, microtasks are executed at the end of each event loop cycle. Before the next experiment, let's create a function to display the cycle of microtasks.

const tick = () => {
  let i = 1;
  const exec = async () => {
    await Promise.resolve();
    console.log(i++);
    if (i < 5) {
      exec();
    }
  };
  exec();
};

Running the tick() function outputs a log for each microtask as follows:

1
2
3
4
5

Now, let's return to the topic of return await. We will call the tick() function alongside the code from the beginning to see at which microtask's timing the processing of the foo function completes.

const foo = async () => {
  return await Promise.resolve(42);
};

const tick = () => {
  // ...
};

const main = async () => {
  await foo();
  console.log("function foo finished");
};

tick();
main();

The results are as follows:

1
2
function foo finished
3
4

Since function foo finished is displayed after 2, we can see that the foo() function completes at the timing of the second microtask.

Next, let's modify the foo() function so that it does not use return await.

const foo = () => {
  return Promise.resolve(42);
};

const tick = () => {
  // ...
};

const main = async () => {
  await foo();
  console.log("function foo finished");
};

tick();
main();

The results are as follows:

1
function foo finished
2
3
4

This time, function foo finished is executed after 1. In other words, when using return compared to return await, the processing of foo() completes one microtask earlier.

This is what it means when we say return await is redundant. Using return await causes an extra microtask to occur.

One interesting specification is that if you do not use return await in an async function, it will be executed one microtask later than if you had used return await.

const foo = async () => {
  return Promise.resolve(42);
};

const tick = () => {
  // ...
};

const main = async () => {
  await foo();
  console.log("function foo finished");
};

tick();
main();
1
2
3
function foo finished
4

This is related to the fact that when an async function returns a value, it is implicitly wrapped in Promise.resolve using the original return value.

In other words:

async function foo() {
  return 1;
}

can be conceptually understood as the following code:

function foo() {
   return Promise.resolve(1)
}

Furthermore, when you return a Promise in an async function, it consumes one extra microtask to wait for "the execution of then on the Promise used for the return" and another extra microtask for "the execution of the callback registered by then."

In the case of return await, it returns a settled Promise, so it consumes one microtask when awaiting, but since it doesn't perform the microtask consumption that occurs when returning a Promise from an async function, it ends up being one microtask faster.

The results are summarized below:

Microtasks
return 1
return (async function) 3
return await (async function) 2

However, note that being executed one microtask later is a matter of priority among asynchronous tasks and does not necessarily directly translate to a performance issue.

Summary

We have discussed the differences between return and return await. Basically, if you don't need to use try/catch inside the function, writing await is redundant.

Fortunately, there is an ESLint rule that prohibits unnecessary return await, so if you are interested, you might want to apply it.

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

However, keep in mind that even if you don't use try/catch inside the function, not using return await can be disadvantageous in terms of stack traces.

References

GitHubで編集を提案

Discussion

PADAone🐕PADAone🐕

マイクロタスクの可視化の項目のコードについて、もしかしたら間違いではないかと思う箇所あったのでコメントさせていただきます。

foo 関数の
return await の話として最初に次のコード

const foo = async () => {
  return Promise.resolve(42);
};

const tick = () => {
  // ...
};

const main = async () => {
  await foo();
  console.log("function foo finished");
};

main();
tick();

そして、その比較として次のコード

const foo = () => {
  return Promise.resolve(42);
};

const tick = () => {
  // ...
};

const main = async () => {
  await foo();
  console.log("function foo finished");
};

tick();
main();

をあげられていますが、コードの違いとしては tick()main() の順番が変わって、foo 自体も Async function ではなくなっています。

-const foo = async () => {
-  return Promise.resolve(42);
-};
+const foo = () => {
+  return Promise.resolve(42);
+};

const tick = () => {
  // ...
};

const main = async () => {
  await foo();
  console.log("function foo finished");
};

-main();
tick();
+main();

しかし、記事内にて

次に foo() 関数を return await しないように修正します。

とあり、さらに冒頭のコードではつぎのような Async function 内での returnreturn await の比較になっています。

const fetchUsers1 = async () => {
  return axios.get("/users");
};

// または

const fetchUsers2 = async () => {
  return await axios.get("/users");
};

従って、記事の文脈的に Async function における returnreturn await の違いの説明をしていると思われますので、コードの比較としてはつぎのようになるのではないでしょうか?

const foo = async () => {
- return await Promise.resolve(42);
+ return Promise.resolve(42);
};

const tick = () => {
  let i = 1;
  const exec = async () => {
    await Promise.resolve();
    console.log(i++);
    if (i < 5) {
      exec();
    }
  };
  exec();
};

const main = async () => {
  await foo();
  console.log("function foo finished");
};

tick();
main();

ただこの場合、手元で実際に実行してみますと return の方では deno でも node でも同じように次の出力となりました。

# return のみの場合
 deno run difftest.js
1
2
3
function foo finished
4

一方、return await の方も実行してみると次の出力になります。

# return await の場合
 deno run difftest.js
1
2
function foo finished
3
4

この場合、return await の方が早く終ることから、「return await は冗長ではない」という結論になるのではないでしょうか?

ただ、記事内でつぎのようにも説明されているので混乱してしまいました。

1 点興味深い仕様として async 関数で return await しなかった場合には return await した場合よりも 1 つ後のマイクロタスクで実行されます。

私はコードの比較自体が単純なミスなのではないかと推測したのですが、もしコードの比較が意図通りのものだとしたら、記事の結論としては「Promise を返す通常の関数と return await する Async function を比較したときに、後者は冗長である」ということを意味しているのでしょうか?

azukiazusaazukiazusa

をあげられていますが、コードの違いとしては tick() と main() の順番が変わって、foo 自体も Async function ではなくなっています。

tick()main() の順番が変わっているのは誤りです🙇‍♂️ ありがとうございます。tick()main() の順番で実行するのが正しいです。

async の有無についてですが記事内で統一性がなく混乱させてしまいました。「マイクロタスクのタイミング」のケースの場合にだけ「通常の関数で Promisereturn した場合」「async 関数で Promisereturn した場合」「async 関数で Promiseawait return した場合」の3つの比較となっております。

「return await は冗長」という主張は「通常の関数で Promisereturn した場合」と「async 関数で Promiseawait return した場合」との比較した場合の結論となっております。

PADAone🐕PADAone🐕

なるほど、理解することができました。ありがとうございます👍

PADAone🐕PADAone🐕

読み直してみたのですが、コメントで指摘した「マイクロタスクの可視化」の項目において、最初のコードで return await Promise.resolve(42); ではなく、return Promise.resolve(42); になってしまっています。これによってコード上では「async 関数で Promisereturn した場合」と「通常の関数で Promisereturn した場合」の比較になってしまっています。なのでawait を付け忘れているのではないでしょうか?

azukiazusaazukiazusa

本当ですね await をつけ忘れていました🙇‍♂️
ご指摘ありがとうございます!

PADAone🐕PADAone🐕

やっぱりそうでしたか!
ちょうど非同期処理について学習していて、実際に実行したら違和感があったので気づいてよかったです👍