iTranslated by AI
3 Differences Between return and return await
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.

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.
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.
Discussion
マイクロタスクの可視化の項目のコードについて、もしかしたら間違いではないかと思う箇所あったのでコメントさせていただきます。
foo関数のreturn awaitの話として最初に次のコードそして、その比較として次のコード
をあげられていますが、コードの違いとしては
tick()とmain()の順番が変わって、foo自体も Async function ではなくなっています。しかし、記事内にて
とあり、さらに冒頭のコードではつぎのような Async function 内での
returnとreturn awaitの比較になっています。従って、記事の文脈的に Async function における
returnとreturn awaitの違いの説明をしていると思われますので、コードの比較としてはつぎのようになるのではないでしょうか?ただこの場合、手元で実際に実行してみますと
returnの方ではdenoでもnodeでも同じように次の出力となりました。一方、
return awaitの方も実行してみると次の出力になります。この場合、
return awaitの方が早く終ることから、「return awaitは冗長ではない」という結論になるのではないでしょうか?ただ、記事内でつぎのようにも説明されているので混乱してしまいました。
私はコードの比較自体が単純なミスなのではないかと推測したのですが、もしコードの比較が意図通りのものだとしたら、記事の結論としては「Promise を返す通常の関数と
return awaitする Async function を比較したときに、後者は冗長である」ということを意味しているのでしょうか?tick()とmain()の順番が変わっているのは誤りです🙇♂️ ありがとうございます。tick()→main()の順番で実行するのが正しいです。asyncの有無についてですが記事内で統一性がなく混乱させてしまいました。「マイクロタスクのタイミング」のケースの場合にだけ「通常の関数でPromiseをreturnした場合」「async関数でPromiseをreturnした場合」「async関数でPromiseをawait returnした場合」の3つの比較となっております。「return await は冗長」という主張は「通常の関数で
Promiseをreturnした場合」と「async関数でPromiseをawait returnした場合」との比較した場合の結論となっております。なるほど、理解することができました。ありがとうございます👍
読み直してみたのですが、コメントで指摘した「マイクロタスクの可視化」の項目において、最初のコードで
return await Promise.resolve(42);ではなく、return Promise.resolve(42);になってしまっています。これによってコード上では「async関数でPromiseをreturnした場合」と「通常の関数でPromiseをreturnした場合」の比較になってしまっています。なのでawaitを付け忘れているのではないでしょうか?本当ですね
awaitをつけ忘れていました🙇♂️ご指摘ありがとうございます!
やっぱりそうでしたか!
ちょうど非同期処理について学習していて、実際に実行したら違和感があったので気づいてよかったです👍