📘

await と then() の違いから学ぶ async/await の使い方

に公開1

awaitthen() の決定的な違い

非同期関数 (Promise を返す関数) を処理したいとき、行単位で見ると、JavaScript のモダンな方法では下記 2つが考えられます。

  1. 非同期関数の前に await をつける ( await AsyncFunction() )
  2. 非同期関数を then() メソッドでチェーンする ( AsyncFunction().then() )

この2つの方法の決定的な違いは、その行が非同期関数の実行の完了を待つかどうかです。
await を使った場合は、非同期関数が完了するまで次の行に進まないため同期処理になるのに対し、then() を使った場合は、非同期関数の完了を待たずに次の行に進むため、非同期処理になります

それぞれの例を、下記の 1秒後に resolve する非同期関数 resolveAfter1Second() を使って説明します。

//  1秒後に resolve する Promise を返す関数
const resolveAfter1Second = () =>
  new Promise((resolve) => {
    setTimeout(() => {
        resolve("resolved");
    }, 1000);
  });

1. await をつける

await をつけた場合、非同期関数の処理が終わるまで次の行に進みません。

console.log("start");
const result = await resolveAfter1Second();
console.log(result);
console.log("end");

出力結果

 "start"
 "resolved"
 "end"

2. then() でチェーンする

then() でチェーンした場合、非同期処理が終わるのを待たずに次の行に進みます。

console.log("start");
resolveAfter1Second().then((result) => {
  console.log(result);
});
console.log("end");

出力結果

 "start"
 "end"
 "resolved"

async/await の使い方

上記のサンプルの通り、 await を使用した場合、非同期関数の処理が終わるまで次の行に進まないため、同期処理になってしまいます。

正しく非同期処理にするためには、 非同期関数の処理を待ってから実行してほしい行を async 関数で切り出す ことで、呼び出し側からはその行の完了を待たずに次の行に進む、という非同期処理を実現できます。

つまり、 非同期関数と共に then() のコールバック関数 (引数に渡す関数) の部分を async 関数で切り出し、その中で await を使うことで、非同期処理を実現できます。
これが、「同期的に非同期処理を実現する」 async/await の使い方です。

例えば、 2. then() でチェーンする と同じ出力結果を await で得るには、次のように 「非同期関数 + その結果のログ出力」を async 関数で切り出します。

//  非同期処理としてまとめたい箇所を async 関数で切り出す
const showResult = async () => {
  const result = await resolveAfter1Second();
  console.log(result);
}

console.log("start");
showResult();
console.log("end");

出力結果

 "start"
 "end"
 "resolved"

まとめ

本記事では、JavaScriptにおける非同期処理の2つの方法、async/awaitthen()の違いについて、 awaitthen の動作の違いから説明してみました。

  • await: 非同期処理が完了するまで次の行に進まない(同期的な振る舞い)
  • then(): 非同期処理の完了を待たずに次の行に進む(非同期的な振る舞い)

という違いから、 await およびその後の関連する処理を async 関数で切り出すことで、非同期処理が実現できることを説明しました。

ES2022 から Top Level Await が導入されたことにより、 モジュールのトップレベルでは then() と同じノリで await が書けてしまうので、知らず知らずのうちに同期処理になってしまうことがあるかもしれません。
そのため、 await を使う際は、非同期処理としてまとめたい箇所を async 関数で切り出すことを意識すると良いと思います。

GitHubで編集を提案

Discussion

junerjuner

非同期処理が完了するまで次の行に進まない(同期的な振る舞い)

await null でわかるように 最低限 queueMicrotask 相当が保証されるので thennable(※Promise 等) でなくても非同期処理を保証するというニュアンスもありますね。

動作仕様でいうと

await Promise.resolve(null);

相当となるので .then で表現すると

Promise.resolve(null).then(() => ... )

相当でしょうか。

https://developer.mozilla.org/ja/docs/Web/API/Window/queueMicrotask

まぁ、 queueMicrotask 自体は then の内部実装が公開されたものですが……。

await の動作仕様自体はここらへん参照のこと

https://tc39.es/ecma262/multipage/control-abstraction-objects.html#await

Promise の値の取得時にそれ自体が Promise でない場合の仕様については PromiseResolve あたりを参照のこと。

https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promise-resolve