はじめに
筆者は、プログラミングスクールで JavaScript の非同期処理を学んでおり、その中である疑問にぶつかりました。
Promise のあとに async/await を学んで、「Promise をシンプルに書ける構文」という説明は理解できたのですが、await が何を止めているのか?という疑問です。
そこがあいまいなままでした。
なんとなく「await は任意のタイミングで処理を止めてくれて、デバッグしやすいのだろう」という認識でいたのですが、それではモヤモヤが残るので、ちゃんと理解したいと思い、今回の記事を書こうとなりました!📝
自分で小さなコードを書いて、console.log の出力順を1行ずつ追ってみました。
この記事では、その結果をもとに
-
awaitは何を止めているのか - Promise(
then)で書いた場合と、何が違うのか
を整理していきます!🐾
本記事は、学習内容の整理と文章の構成に AI(Claude)を利用しました。掲載しているコードは手元で動作を確認しています。
対象読者
- Promise と async/await を一通り学んだけど、動きのイメージがあいまいな人
- 「
awaitで待つ」の「待つ」が何を指しているのか、しっくりきていない人
結論
await で一時停止するのは、その async 関数の中だけ。
関数の外のコードは待たずに先へ進むので、待っている間に画面が固まることはありません。
そして、Promise(then)との一番の違いは次の点です。
async/await では、関数の中で書いた順番 = 実行される順番 になる。
1. 準備:指定した時間だけ待つ関数
この記事では「お茶を淹れる」流れを例にします。まず、時間のかかる作業(お湯を沸かす、など)の代わりに、指定した時間だけ待つ関数を用意します。
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
wait(3000) と呼ぶと、「3秒後に成功する Promise」が返ってきます。
setTimeout(resolve, ms) は「ms ミリ秒たったら resolve を呼んで」という意味です。resolve() ではなく resolve と書いているので、今すぐ呼ぶのではなく、関数そのものをタイマーに渡しています。
2. 実験:出力順を見てみる
console.log の先頭に付けた番号は、実行される順番です。書いてある順番と見比べてみてください。
async function makeTea() {
console.log("1. お湯を沸かし始める");
await wait(3000); // お湯が沸くまで3秒待つ
console.log("3. お湯が沸いた");
console.log("4. お茶を淹れる");
}
makeTea();
console.log("2. 待っている間に、湯のみを用意する");
1. お湯を沸かし始める
2. 待っている間に、湯のみを用意する
3. お湯が沸いた
4. お茶を淹れる
「2.」はコードの一番最後に書いてあるのに、2番目に出ています。
もし await がプログラム全体を止めるなら、「2.」は3秒待ってから、一番最後に出るはずです。先に出ているということは、止まっていたのは makeTea の中だけだった、ということになります。
3. 何が起きているのか
時間の流れに沿って図にすると、こうなります。
await に来た関数は、そこに「しおり」を挟んで、いったん呼び出し元に戻ります。Promise が成功したら、しおりの位置から続きを再開します。
しおりを挟んでいる間も、関数の外のコード(湯のみの用意)は普通に進みます。だから、待っている間に画面が固まることはありません。
await は、非同期処理を「同期処理に戻す」ものではありません。止まるのは async 関数の中だけで、JavaScript 全体は動き続けています。
最初は「await = 好きなところで処理を止める(デバッグ用の)機能」だと思っていましたが、実際には「Promise の結果が出るまで、その関数の中だけ続きを待つ」仕組みでした。処理を一時停止して変数の中身を確認したいときに使うのは、await ではなくデバッガーのブレークポイント(または debugger; 文)です。
4. Promise(then)で書くとどうなるか
同じ流れを then で書いてみます。今度は「湯のみを用意する」を、関数の中に書きました。
function makeTea() {
console.log("1. お湯を沸かし始める");
wait(3000).then(() => {
console.log("3. お湯が沸いた");
console.log("4. お茶を淹れる");
});
console.log("2. (関数の中)湯のみを用意する");
}
makeTea();
1. お湯を沸かし始める
2. (関数の中)湯のみを用意する
3. お湯が沸いた
4. お茶を淹れる
関数の中に書いた「2.」が、then の中身より先に出ています。
違いは「どこまでが待ったあとの処理か」
| 書き方 | 「待ったあと」に実行されるのは |
|---|---|
| then |
then に渡した関数の { } の中だけ
|
| async/await |
await より下に書いたすべて
|
-
then:
.then(...)は「終わったらこれをして」と予約するだけです。予約したらすぐ次の行に進むので、thenの外に書いた行は待たずに実行されます -
async/await:
awaitで関数そのものが一時停止するので、その下に書いた行は全部「待ったあと」になります
だから async/await では、書いた順番 = 実行される順番 になり、上から順に読むだけで動きが分かります。
一方で、関数の外から見ると、どちらも同じです。呼び出したらすぐに戻ってきて、外のコードは待たずに進みます。
5. 結果を受け取るときの違い
今度は、Promise が成功したときに値を渡してくれる関数を使います。
function boilWater() {
return new Promise((resolve) => {
setTimeout(() => resolve("沸かしたお湯"), 3000);
});
}
then の場合:引数で受け取る
boilWater().then((water) => {
console.log(`${water}でお茶を淹れる`);
});
async/await の場合:変数で受け取る
async function makeTea() {
const water = await boilWater();
console.log(`${water}でお茶を淹れる`);
}
沸かしたお湯でお茶を淹れる
resolve("沸かしたお湯") に渡した値が、then では引数 water に、async/await では変数 water に入ります。
結果が2つ以上になると、差が大きくなる
「お湯」と「茶葉」の両方を最後に使いたい場合を考えます。
// then の場合:water は最初の then の中でしか使えない
let water;
boilWater()
.then((result) => {
water = result; // 外の変数に入れておく必要がある
return getTeaLeaves(true); // 茶葉を取る(6章で定義)
})
.then((leaves) => {
console.log(`${water}と${leaves}でお茶を淹れる`);
});
// async/await の場合:普通の変数なので、そのまま使える
async function makeTea() {
const water = await boilWater();
const leaves = await getTeaLeaves(true);
console.log(`${water}と${leaves}でお茶を淹れる`);
}
then の引数は、その { } の中でしか使えません。async/await なら、同じ関数の中の普通の変数として、下の行のどこでも使えます。
6. エラー処理の違い
茶葉が切れていたら失敗する関数を用意します。
function getTeaLeaves(hasLeaves) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (hasLeaves) {
resolve("緑茶の茶葉");
} else {
reject(new Error("茶葉が切れていた"));
}
}, 1000);
});
}
then の場合:.catch / .finally をつなぐ
getTeaLeaves(false)
.then((leaves) => console.log(`${leaves}でお茶を淹れる`))
.catch((error) => console.log(`${error.message}ので、麦茶にする`))
.finally(() => console.log("やかんの火を止める"));
async/await の場合:try / catch / finally で囲む
async function brew(hasLeaves) {
try {
const leaves = await getTeaLeaves(hasLeaves); // 失敗したら、ここから catch へジャンプ
console.log(`${leaves}でお茶を淹れる`);
} catch (error) {
console.log(`${error.message}ので、麦茶にする`);
} finally {
console.log("やかんの火を止める"); // 成功でも失敗でも必ず
}
}
brew(true);
brew(false);
緑茶の茶葉でお茶を淹れる
やかんの火を止める
茶葉が切れていたので、麦茶にする
やかんの火を止める
| 役割 | then | async/await |
|---|---|---|
| 成功したとき | .then(...) |
try の中の await の下 |
| 失敗したとき | .catch(...) |
catch (error) { } |
| どちらでも最後に | .finally(...) |
finally { } |
try / catch は、await の失敗だけでなく、try の中で起きた普通のエラー(存在しないメソッドを呼んだ、など)もまとめて捕まえられます。
7. 順番に待つか、同時に待つか
「お湯を沸かす(3秒)」と「湯のみを温める(2秒)」は、お互いに関係ない作業です。
function warmCup() {
return new Promise((resolve) => {
setTimeout(() => resolve("温めた湯のみ"), 2000);
});
}
await を順番に並べると、合計時間がかかる
async function sequential() {
const start = Date.now();
await boilWater(); // 3秒
await warmCup(); // 2秒(お湯が沸いてから始まる)
console.log(`順番に待つ: 約${Math.round((Date.now() - start) / 1000)}秒`);
}
Promise.all を使うと、同時に進む
async function parallel() {
const start = Date.now();
const [water, cup] = await Promise.all([boilWater(), warmCup()]);
console.log(`同時に待つ: 約${Math.round((Date.now() - start) / 1000)}秒`);
}
順番に待つ: 約5秒
同時に待つ: 約3秒
await を順番に並べると、1つ終わるまで次が始まらないので、3 + 2 = 5秒かかります。Promise.all の中で2つを続けて呼ぶと、両方のタイマーがほぼ同時に動き出し、それをまとめて待つので、長いほうの3秒で済みます。
| 状況 | 書き方 |
|---|---|
| 前の結果を次で使う(お湯が沸いてから、お茶を淹れる) |
await を順番に並べる |
| お互いに関係ない(お湯を沸かす / 湯のみを温める) |
await Promise.all([...]) で同時に |
async/await は「順番に待つ」のが得意ですが、「同時に待つ」ときは Promise の機能(Promise.all)をそのまま使います。async/await を使っていても、Promise の知識は必要だということです。
8. Promise(then)との違いまとめ
| 観点 | then | async/await |
|---|---|---|
| 中身 | Promise | 同じ Promise |
| 「待ったあと」の範囲 |
then の { } の中だけ |
await より下の全部 |
| 書いた順と実行順 | ずれることがある | 関数の中では一致する |
| 結果の受け取り | 引数(その { } の中だけ) |
変数(関数の中のどこでも) |
| エラー処理 |
.catch / .finally
|
try / catch / finally
|
| 同時に待つ | Promise.all(...).then(...) |
await Promise.all(...) |
| 関数の外から見た動き | 待たずに進む | 待たずに進む(同じ) |
できることは同じで、違うのは書き方と読みやすさです。
おまけ:「止まるのは関数の中だけ」と分かると理解できること
await を付け忘れると、Promise がそのまま入る
async function makeTea() {
const water = boilWater(); // await を付け忘れた
console.log(water); // Promise {<pending>}
}
await がないと関数は止まらないので、まだ結果が出ていない Promise がそのまま変数に入ります。
async 関数の戻り値は、必ず Promise になる
async function getTea() {
return "緑茶";
}
console.log(getTea()); // Promise {<fulfilled>: '緑茶'}
async 関数は、await を使っているかどうかに関係なく、必ず Promise を返す決まりになっています。途中で一時停止して呼び出し元に戻ることがあるので、結果はあとから Promise で受け取る、と考えると覚えやすいです。中身を使うには、呼ぶ側でも await するか .then で受け取ります。
まとめ
-
awaitで一時停止するのは、その async 関数の中だけ。関数の外は待たずに進むので、待っている間に画面は固まらない - async/await では、関数の中で書いた順番 = 実行される順番になる。then では
thenの中だけが「あと」で実行される - 結果は普通の変数で受け取れて、エラーは
try/catchで受けられる。同時に待つときはPromise.allを使う
最初は「await = 処理を止める機能」くらいの理解でしたが、出力順を1行ずつ追ってみたことで、「await で待つ」の意味がはっきりしました。