async / await が生み出す「同期っぽいコード」とは
async / await を使うと、await を並べるだけで、「上から順番に実行されているように見えるコード」 が書けます。
async function run() {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log("完了");
}
JavaScript見た目はほぼ同期処理です。
でも、実際は「イベントループの上で動く非同期処理」であり、完全な同期とは世界が違います。
ここが重要です。
async / await は「見た目を同期っぽくする道具」であって、
処理そのものを同期化する魔法ではありません。
“同期風に見えるからこそハマる罠” を先に知っておくと、非同期コードで迷子になりにくくなります。
注意点1 「同期っぽく見えるけど、関数全体は非同期」
async 関数の外から見ると「Promise の世界」
async 関数の中では、await のおかげで「同期っぽく値を扱える」ようになります。
async function getUserName() {
const user = await fetchUser();
return user.name;
}
JavaScript中だけ見れば、「ユーザーを取り出して name を返す関数」です。
でも、外から見ると戻り値は string ではなく Promise です。
const name = getUserName(); // string ではない
console.log(name); // Promise {...}
JavaScript同期コードの感覚で name を「普通の値」として使おうとすると、
ここでつまずきます。
正しい扱い方は、外側でも await することです。
async function main() {
const name = await getUserName();
console.log(name);
}
JavaScriptここが重要です。
中は同期風でも、外から見れば常に「Promise を返す関数」。
async 関数にちょっと変えただけで、“戻り値の世界” が完全に変わることを意識してください。
注意点2 「全部直列 await」による無駄な遅さ
何も考えずに await を並べると「のろのろ行列」になる
async / await に慣れてくると、
ついこう書きがちです。
async function loadAll() {
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();
return { a, b, c };
}
JavaScriptこれは「同期っぽくて読みやすい」反面、
A → B → C を完全に直列で実行する ことになります。
もし A・B・C が互いに独立した API なら、
本当は同時に叩けたはずです。
async function loadAllFast() {
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC(),
]);
return { a, b, c };
}
JavaScript「同期風の書きやすさ」と「パフォーマンス」は別物
async / await の良さは「読みやすさ」ですが、
そのまま書くと「常に直列」になり、無駄に遅くなる 場面が出てきます。
順番が本当に必要なのか、
ただ「同期っぽくて気持ちいいから」直列にしているだけなのか。
ここを一度立ち止まって考えるクセが大事です。
ここが重要です。
“同期風コード” は、人間に優しいがマシンに優しいとは限りません。
依存関係のない非同期処理は、Promise.all などで意識して並列化しないと、「きれいだけど遅いコード」になります。
注意点3 await を「どこでも」使えると思わない
await は async 関数の中だけで有効
よくある勘違いが、
「await は書きたければどこでも書ける」と思ってしまうことです。
// これは文法エラー
const data = await fetchData();
JavaScript関数の外で await は使えません(トップレベル await 対応環境を除く)。
必ず async 関数の中に閉じ込める必要があります。
async function main() {
const data = await fetchData();
console.log(data);
}
main();
JavaScript「非同期処理をしている関数」を境界として意識する
同期風に書けるからこそ、
どこまでを async 関数にして、どこまでを普通の関数にするかが設計ポイントです。
なんでもかんでも async にすると、
「戻り値が全部 Promise だらけ」という地獄 になります。
逆に、
非同期処理をしているのに、
外側のコードがそれを意識せず同期扱いしていると、
「Promise が返ってきたまま放置」というバグにつながります。
ここが重要です。
await を使える範囲は「async 関数の中だけ」。
その境界を意識して、「ここから中は非同期の世界」「ここから外は Promise を扱う世界」と頭で切り分けてください。
注意点4 「await したらスレッドが止まる」と思わない
await で止まるのは「その async 関数の続き」だけ
await は、「処理を止めている」ように見えます。
async function run() {
console.log("A");
await wait(1000);
console.log("B");
}
JavaScriptログは「A →(1秒後)→ B」と出ます。
ここだけ見ていると、
「ああ、コード全体が止まってるんだな」と感じやすいですが、
実際には止まっているのはこの関数の「続き」だけです。
他のイベント処理や描画、ユーザー操作の処理などは、
普通に動き続けています。
「スレッドがブロックされている」わけではありません。
「同期風でも、重い同期処理」は依然として危険
逆に、本当に重たい同期処理を async 関数の中に書いてしまうと、
普通に UI が固まります。
async function bad() {
// これは await してない、ただの重い同期処理
for (let i = 0; i < 1_000_000_000; i++) {
// 重い計算
}
console.log("終わり");
}
JavaScriptasync を付けても、
重い処理が非同期になるわけではありません。
ここが重要です。
await は「非同期処理を待つための一時停止ボタン」であって、
「重い処理を軽くする魔法」ではありません。
重い同期処理は async の中にあっても危険、
await は Promise を“待つ”もの、と切り分けましょう。
注意点5 try / catch 範囲の勘違いとエラーの「すり抜け」
await の外で起きるエラーはその try / catch では取れない
同期風に見えるため、
try / catch の範囲を勘違いしやすくなります。
async function run() {
try {
const user = await fetchUser(); // ここは捕まえられる
} catch (err) {
console.error("ここで捕まえたい");
}
const posts = await fetchPosts(); // ここで失敗しても上の catch には飛ばない
}
JavaScriptfetchPosts() のエラーを捕まえたいなら、
その await も try の中に入れる必要があります。
async function run() {
try {
const user = await fetchUser();
const posts = await fetchPosts();
} catch (err) {
console.error("どちらかのエラー:", err);
}
}
JavaScriptcatch で握りつぶしているつもりで、実は返していることもある
もうひとつは、「async 関数の戻り値が Promise」だという性質との組み合わせです。
async function foo() {
try {
await something();
} catch (err) {
console.error("中でログだけ出す");
}
}
JavaScriptこの foo は、
エラー発生時も「resolve(成功扱い)の Promise」を返します。
呼び出し側は await foo() してもエラーにならないので、
「エラーがあったかどうか分からない」状態です。
ここが重要です。
「同期風の try / catch」で安心していると、
実はエラーを上に伝えられていない、ということがあります。
“この async 関数は、エラーを外に伝えるのか、ここで完結させるのか” をハッキリ決めて書いてください。
注意点6 「await し忘れ」で意図しない並列が起きる
await を付けたつもりで付けていない
コードを同期風に書いていると、
うっかり await を付け忘れることがあります。
async function run() {
const data = fetchData(); // await を忘れた
console.log(data); // Promise が出てくる
}
JavaScriptこれはすぐ気付きますが、
もっと厄介なのは「順番にやるつもりだったのに、実は同時に走り出している」ケースです。
async function run() {
doAsyncA(); // await なし → すぐ次へ
await doAsyncB(); // B を待っている間も A は裏で動いている
}
JavaScript本人は「A が終わってから B のつもり」でも、
実際は「A と B が並列で動いて、B だけ await」になっています。
ここが重要です。
同期風に見えるからこそ、「await を付けたところで止まる」「付けなければそのまま進む」という基本を崩さないこと。
“ここは順番が大事だ” と思ったら、「本当に全ての非同期呼び出しに await を付けているか」を丁寧に確認してください。
初心者として「同期風コードの注意点」で本当に押さえてほしいこと
async / await は、見た目を「同期風」にしてくれるだけで、
処理自体を同期に変えるわけではない。
async 関数の戻り値は常に Promise。
中で同期みたいに書けても、外から見ると「Promise の世界」になる。
何も考えずに await を縦に並べると、全部直列になり、無駄に遅くなることがある。
依存関係のない処理は Promise.all などで並列化することも検討する。
await は async 関数の中だけで使える。
「どこからどこまでが async の世界か(境界)」を意識して設計する。
try / catch の範囲に入っていない await のエラーは捕まえられない。
どこでエラーを扱い、どこまで伝えるかを決めて書く。
ここが重要です。
async / await の「同期風な書き心地」は最高の武器ですが、
“あくまで見た目だけ” だと割り切ることが大切です。
「これは本当に同期っぽく動いていいのか?」
「どこからどこまでが Promise の世界なのか?」
この 2 点を常に意識しながら書けば、
async / await はあなたにとって“怖いもの”ではなく、“気持ちよく制御できる道具”になっていきます。
もし練習したくなったら、こんなことをやってみてください。
// 1. fetchA, fetchB, fetchC をそれぞれ1秒待って文字列を返す関数として作る。
// 2. 直列(順番に await)、Promise.all(完全並列)、そして「await を1つだけ外したパターン」を試し、
// console.time で時間を測りながら挙動とログを観察する。
// 3. 「自分が意図したのはどれだったか?」を振り返り、コメントとして書いてみる。
JavaScript自分の「意図」と「現実の動き」の差を一度でもハッキリ確認しておくと、
同期風コードの注意点が、頭だけでなく感覚としても掴めるようになります。
