return と return await の3つの違い
Promise
を返す非同期関数を扱うとき Promise
をそのまま返す書き方と Promise
を await
してから返す二通りの方法があります。
const fetchUsers1 = async () => {
return axios.get("/users");
};
// または
const fetchUsers2 = async () => {
return await axios.get("/users");
};
上記のコード例は一見するとどちらも同じように動作するように思えますが、以下のような違いがあります。
- 例外がキャッチされる場所
- スタックトレースの出力
- マイクロタスクのタイミング
例外がキャッチされる場所
初めに考えられるもっとも顕著な例として try/catch
ブロックと共に使用されている場合です。端的に説明すると return
の場合には関数の呼び出し元で例外がキャッチされますが await return
の場合には関数の内部で例外がキャッチされます。
以下の例で確認してみましょう。alwaysRejectPromise
関数は常に Promise
を拒否します。
const func1 = async () => {
try {
return alwaysRejectPromise();
} catch (e) {
console.log("func1 の中で catch");
}
};
const func2 = async () => {
try {
return await alwaysRejectPromise();
} catch (e) {
console.log("func2 の中で catch");
}
};
func1().catch((e) => console.log("func1 の呼び出し元で catch"));
func2().catch((e) => console.log("func2 の呼び出し元で catch"));
実行結果は以下のとおりです。
func2 の中で catch
func1 の呼び出し元で catch
期待通り、Promise
を単に return
している func1
の場合には関数の呼び出し元の catch
句で例外が捕捉され、await return
している func2
の場合には func2
の内部の catch
句で例外が捕捉されています。
どうしてこのような違いが生じるのでしょうか?await 演算子は Promise
が決定される(履行または拒否されるまで)まで待機することがポイントです。
つまりは await return
の場合には Promise
が完了するまで関数内で待機するため Promise
が拒否されたときにはまだ処理が関数内に残っています。そのため関数内の catch
句で例外が捕捉されます。
一方で return
の場合には関数内で Promise
が完了するまで待機されません。そのため Promise
が拒否されたときにはすでに関数内での処理は終了してしまっています。ですから関数内の catch
句ではなく呼び出し元の catch
句で例外が捕捉されるのです。
スタックトレースの出力
2 つ目の違いとしてはスタックトレースの出力の違いがあげられます。これはさきほどの例外がキャッチされる場所と同じ原理です。実施のコード例で確認してみましょう。始めに 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));
スタックトレースは次のように出力されます。
Error: something went wrong
at bar (index.js:7:9)
これを関数 foo()
内で return await
を使用するように修正します。
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));
スタックトレースの詳細は以前のバージョンに比べてより詳細なものになります。
Error: something went wrong
at bar (index.js:7:9)
at async foo (index.js:2:10)
マイクロタスクのタイミング
単純に述べると return await
は冗長です。次のコードは関数内部で Promise
が完了するのを待機した後、関数の呼び出し元でもう一度 Promise
が完了するのを待つ必要があります。
const foo = async () => {
return await Promise.resolve(42);
};
const main = async () => {
await foo();
console.log("function foo finished");
};
main();
このことがどのように問題となるか理解するには JavaScript におけるマイクロタスクの概念を理解する必要があります。
マイクロタスクとは?
マイクロタスクは簡潔に述べると非同期処理の待ち行列(キュー)の一種です。例えば Promise
や Mutation Observer API
において使用されています。マイクロタスクは先入れ先出しのキューとなっており、Promise
が呼ばれたタイミングではマイクロタスクキューにタスクを登録します。その後マイクロタスクが実行できるタイミングになったらマイクロタスクキューから順番にタスクを呼び出して実行します。
マイクロタスクが実行できるタイミングは 1 回のイベントループがが終了するタイミングです。イベントループが収集したすべてのタスクが完了した後に保留されていたマイクロタスクの実行を始めます。端的に換言すると、すべての同期処理が完了した後になってはじめて非同期処理(マイクロタスク)が実行されるということです。以下の例で確認してみましょう。
console.log(1); // ①
console.log(2); // ②
Promise.resolve().then(() => console.log(3)); // ③
for (let i = 0; i < 1000000000; i++) {} // ④
console.log(4); // ⑤
始めはコードの順番にならって ①、② の処理が実行されます。その後 ③ の処理ではマイクロタスクキューにタスクが登録されこの時点ではまだ実行されません。その後興味深いのは ④ の処理でありここでは大量の for
ループで処理をブロッキングしています(ここでは大雑把に 3000ms かかると仮定しましょう)。③ の処理の実行は実際には 1ms で完了するのですが、マイクロタスクキューに登録されたタスクは現在のイベントループのすべてのタスクの完了を待たなければいけません。3000ms が経過した後にようやく ⑤ の処理が実行されその後にマイクロタスクの実行が開始されるので最後に 3
が出力されます。
マイクロタスクの可視化
マイクロタスクは前述のとおり各イベントループの終了時に実行されます。次に行う実験の前にマイクロタスクの周期を表示する関数を作成します。
const tick = () => {
let i = 1;
const exec = async () => {
await Promise.resolve();
console.log(i++);
if (i < 5) {
exec();
}
};
exec();
};
tick()
関数を実行すると下記のように 1 マイクロタスク毎にログを出力します。
1
2
3
4
5
ここで return await
の話に戻りましょう。冒頭のコードと同時に tick()
関数を呼び出してどのマイクロタスクのタイミングで foo
関数の処理が完了したのかを確認してみましょう。
const foo = async () => {
return await Promise.resolve(42);
};
const tick = () => {
// ...
};
const main = async () => {
await foo();
console.log("function foo finished");
};
tick();
main();
結果は次のとおりです。
1
2
function foo finished
3
4
function foo finished
が 2
の後に表示されていることから 2 回目のマイクロタスクのタイミングで関数 foo()
が実行されていることがわかります。
次に foo()
関数を return await
しないように修正します。
const foo = () => {
return Promise.resolve(42);
};
const tick = () => {
// ...
};
const main = async () => {
await foo();
console.log("function foo finished");
};
tick();
main();
結果は次のとおりです。
1
function foo finished
2
3
4
今度は function foo finished
が 1
の後に実行されています。つまり、return await
と比べて return
する場合には 1 つは早いマイクロタスクで foo()
の処理が完了するということになります。
これが、return await
が冗長ということの意味です。return await
をすることによって余分なマイクロタスクが発生してしまうのです。
1 点興味深い仕様として async
関数で return await
しなかった場合には return await
した場合よりも 1 つ後のマイクロタスクで実行されます。
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
これは async
関数が値を返す際には暗黙的の本来の戻り値を用いて Promise.resolve
されることに関係します。
つまり
async function foo() {
return 1;
}
は擬似的に次のコードと理解できます。
function foo() {
return Promise.resolve(1)
}
さらに async
関数において Promise
を return
した場合には「return
に使われた Promise
に対する then
の実行」を待つのに 1 マイクロタスク「then
で登録されたコールバックの実行」に 1 マイクロタスクを余分に消費します。
return await
の場合には完了済の Promise
を返すので await
する際に 1 マイクロタスクを消費しますが、その後 async関数において
Promiseを
return` した場合のマイクロタスクの消費を実行しないので 1 マイクロタスク早くなるわけです。
結果としては次のようになります。
マイクロタスク | |
---|---|
return | 1 |
return(async 関数) | 3 |
return await(async 関数) | 2 |
ただし、1 つ遅いマイクロタスクで実行されるということは非同期処理感の優先順位にまつわる話であり、必ずしもパフォーマンス上の問題に直結するわけではないことに注意してください。
まとめ
return
と return await
の違いについて述べてきました。基本的には関数内部で try/catch
をする必要がない場合にわざわざ await
を書くのは無駄な処理となります。
幸いなことに無駄な return await
を禁止する ESlint ルールが存在するので興味を持ったかたはこのルールを適用させてみるとよいでしょう。
ただし、try/catch
を関数内部で使用しない場合でも return await
をしない場合にはスタックトレース上不利になることは留意してください。
参考
- Difference between
return await promise
andreturn promise
- Why no-return-await vs const x = await?
- await vs return vs return await
- 'return await promise' vs 'return promise' in JavaScript
- 徹底解説! return promise と return await promise の違い
- JavaScript の非同期処理をじっくり理解する (3) async/await
Author And Source
この問題について(return と return await の3つの違い), 我々は、より多くの情報をここで見つけました https://zenn.dev/azukiazusa/articles/difference-between-return-and-return-await著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol