JavaScript の async / await の使い方について思い違いをしていました。また同じミスを犯さないように、どういうことだったかを以下に書いておきます。
例として、GET 要求を受けると 2 秒後に以下の JSON 文字列を返す Web API から fetch を使って応答を取得するサンプルを書きました。
{"id":5,"name":"スライムマン"}
JavaScript のコードは以下のようにし、ボタンクリックなどのイベントで asyncTest を実行します。
const fetchHero5 = async () => {
const response = await fetch("/api/values/5")
const data = await response.json();
console.log(data.name);
};
const writeLog = () => {
console.log("writeLog の実行");
}
// 同期的に順番に実行される
function asyncTest()
{
fetchHero5();
writeLog();
}
fetchHero5 の中で await で待機しているので、asyncTest の中の fetchHero5 の呼び出しでまず Web API からの応答の "スライムマン" がコンソールに書き込まれ、その後 writeLog が呼び出されて "writeLog の実行" が書き込まれると思っていたのですが、そうではなかったです。
結果は以下の通りで、思っていたのと逆の順序でした。
asyncTest は同期関数でその中の fetchHero5() と writeLog() は同期的に実行されます。つまり、同期的に順番にまず fetchHero5 が呼ばれ、fetchHero5 の完了後 writeLog が呼ばれるという動きになります。
ここで、fetchHero5 は async が付与された非同期関数であるということが、自分が思っていた結果と違ったことになった原因のようです。
現代の JavaScript チュートリアル の記事 Async/await に "async は関数が promise を返すことを保証し、非 promise をその中にラップします" という説明があります。
ということは、fetchHero5 が呼ばれると即 Promise が返され、その時点で fetchHero5 は完了とみなされて、次の writeLog が呼ばれてコンソールに "writeLog の実行" と書き込んだということのようです。
その後、Web API からの応答の処理は fetchHero5 から返された Promise が行うようです。Promise オブジェクトが fetch で Web API を呼んで 2 秒後に応答が返ってきてから "スライムマン" と書き込んだということだと思います。
fetchHero5() で "スライムマン" と書き込んだ後で、writeLog() で "writeLog の実行" と書き込む(上の画像の順序を反対にする)には、asyncTest に async を付与してを非同期関数にし、その中の fetchHero5 に await を付与してそこで待機するようにします。
すなわち以下のようなコードにします。
async function asyncTest()
{
await fetchHero5();
writeLog();
}
結果は以下の通りとなります。
上のコード例で、一番下位の関数 fetch, json は Promise を返します。その上位の関数 fetchHero5 も async が付与されているので Promise を返します。そういう形で Promise が下位から上位に伝搬して行くので、上のコード例でのように最上位の関数 asyncTest を非同期にして、そこで await を使って fetchHero5 の完了を待機すると期待したようになるということのようです。
.NET アプリの Microsoft のドキュメント「非同期プログラミングのベスト プラクティス」には "上から下に (または下から上に) 非同期コードが他の非同期コードを呼び出す、または呼び出される場合に最もうまく機能する" ということがに書かれています。
それは JavaScript でも同じことなのかもしれません。
(自分用のメモ: 検証に使ったのは VS2022 の MvcCore6App2 プロジェクトの Home/Api)