前提
【謝辞】 小学生にわかるように説明できるを目標に書き始めて途中で言いたいことが増えて最終的に小学生置いてけぼりになりそう。
この記事はJavaScriptの同期(どうき)と非同期(ひどうき)は、プログラムがどうやって順番に動くかのお話だよ。
関連記事
🧱 ブラウザの基礎(Rendering Pipeline)
-
ブラウザレンダリングの仕組み(基礎編)
DOM / CSSOM / Render Tree の基礎まとめ -
ブラウザレンダリング(内部構造と最適化)
メインスレッド / コンポジタ / ラスタ / GPU
⚙️ JavaScript と非同期処理
-
JavaScript の実行順
JS が「どの順番で実行されるか」を理解する -
小学生にもわかるかもしれない同期・非同期・Promise
同期と非同期をわかりやすく説明
🚀 パフォーマンスと UX
-
ユーザー中心のパフォーマンス モデル RAIL
UX の体感速度を設計する枠組み -
Core Web Vitals
Google が推す“実測体験”の評価指標
まずは「同期処理(どうきしょり)」って?
同期処理は、「一つずつ順番に」やる方法。
console.log("A");
console.log("B");
console.log("C");
//結果
A
B
C
これは「Aが終わってからB、Bが終わってからC」って感じ。
本を読むときに、ページ1 → ページ2 → ページ3…みたいに順番に読むのと同じ。
じゃあ「非同期処理(ひどうきしょり)」って?
非同期処理は、「外の世界に任せられる処理は後でやるよ!」っていう方法。
たとえば、Webからデータを取ってくるには時間がかかるよね。
その間、プログラムを止めないで、他のことを先にやるのが非同期処理。
ポイント:JavaScript が「同時に2つのことをしてる」わけではないよ。
時間がかかる仕事だけ、いったんブラウザへ預けておいて、終わったら「できたよー!」と順番に戻してくれる仕組みなんだ。
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000); // 1秒後にBが表示される
console.log("C");
// 結果
A
C
B
JavaScript は「1つの流れ(1本の道)」しか同時に進めないんだ。
じゃあなんでBをあとにしてCが先に出るの?と思うけど、それはブラウザが裏で仕事を分けてくれるから。
- JavaScript → 1本の道(順番にしか動けない)
- ブラウザ → たくさんの作業係がいる(タイマー係、通信係 など)
終わった仕事だけ JavaScript の道にもどってくる仕組みなんだ。
イベントループについて
JavaScript は「すぐできる仕事」はその場で実行するけど、時間がかかる仕事はまず キュー(処理の順番待ちの列) に並ぶんだ。
そして「コールスタック(今実行している場所)」が空いたときに、キューの一番前が実行されるしくみになっている。
これをイベントループと言うよ!
※実際には Promise 用の“マイクロタスク”キューなどもあるけど、ここではイメージしやすさを優先してざっくり説明しているよ
時間がかかる処理かどうかどうやってわかるの?
基本的に「プログラムの中だけで完結しないもの」は時間がかかる処理
プログラムの中だけで終わらないものは、外(サーバー・パソコン・ブラウザなど)にお願いして結果を待つから、時間がかかる処理になるんだよ
例①:Webサーバーからデータを取ってくる(API通信)
fetch("https://api.example.com/data")
例②:Webサーバーから画像や動画の読み込み
const img = new Image();
img.src = "big-image.jpg";
ファイルが大きかったり、ネットが遅かったりすると遅くなるよ
例③:ディスクからファイルを読む
※ ブラウザではなくサーバー側 JavaScript の例
fs.readFile("file.txt", (err, data) => { ... });
コンピュータの中にあるファイル(メモ帳とか画像とか)を読み取る処理。コンピュータはファイルを探して、開いて、中身を読まないといけないから少し時間がかかるよ
例④:setTimeoutやsetInterval、requestAnimationFrame
setTimeout(() => {
console.log("3秒後にこれが出る!");
}, 3000);
setTimeout のカウントダウンは JavaScript がやってるんじゃなくて、 ブラウザが裏で「タイマー係」として動かしてくれてるんだ。
console.log("A");
requestAnimationFrame(() => {
console.log("B(画面を描くタイミング)");
});
console.log("C");
// 結果: A → C → B
そしてrequestAnimationFrameは、「次に画面が描き直されるタイミングで呼んでね!」とブラウザにお願いする仕組みだよ。
これらはネットやファイルにはアクセスしないけど、時間の管理 や 画面の更新タイミングをブラウザが別でやってくれる仕組みだから非同期になるよ。
プログラム本体は待っているあいだヒマになるから、その間に他の仕事ができるんだ。
非同期かどうかの見分けポイント
| 処理の種類 | 処理する場所 | 待ち時間ある? | 非同期になる? |
|---|---|---|---|
| ネットワークアクセス | サーバー待ちをする | あり | ✅ なる |
| ファイルの読み書き | パソコン待ちをする | あり | ✅ なる |
| setTimeout/Intervalなど | ブラウザ待ちをする | あり | ✅ なる |
| for / if / + / - | プログラムの中で完結 | なし(とても大きいループを除く) | ❌ |
| 配列の操作(mapなど) | プログラムの中で完結 | なし | ❌ |
※ただし、内部だけの処理でも「めっちゃ重い処理(100万回ループとか巨大なJSONをparseとか)」は時間がかかることがあるよ。でもこれは“外の世界に任せられない”ので非同期にはならず、同期のまま動くんだ。
※ 外の世界を使わなくても「とても重い計算」だと画面が止まることがあるよ。
これは非同期にはならず、JavaScript の道をふさいでしまうんだ。
わからなかったら?
「それが外の世界(ネット、ディスク、画面)と関係あるかどうか」で考えてみて!
- 内部だけで終わる処理 → 同期(重くても自動では非同期にならない)
- 外のものにアクセスする処理 → 非同期
Promise(プロミス)って何?
Promise は「あとで結果が入る箱」。非同期の処理が終わったタイミングで、その箱に結果が入る。
Promise を返す関数を呼んだ瞬間、非同期処理そのものはもう動き始めいて、「あとで結果が入る箱」が返ってくるだけ。
この箱(Promise)は、「成功(resolve)」か「失敗(reject)」、どちらか1回だけ決まる仕組みになっていて、あとから変更はできないんだ。
Promiseの使い方
Promiseには主に3つの使い方があるよ
- Promiseオブジェクトだけ返して後で使う
- async/await+try/catch パターンで順番通り&エラーをまとめて管理
- then/catch パターンで「終わったらこうする/失敗したらこうする」とつなぐ
普段は2や3が実際の利用パターンになることが多く、可読性重視ならasync/await+try/catchが一番オススメ!
※「Promiseが返ってくる」という表現は「あとで結果が返ってくることを約束するオブジェクト(非同期処理)が返る」の意味で、値とかが返ってくるっていう意味じゃない
「catch(キャッチ)」が二回出てきてややこしいね!
-
try/catch の catch
- 普通のエラー(例外)や、awaitで起きた失敗をまとめて受け取るためのもの
- 同期処理にも非同期処理にも使える
- Promiseで使う場合async/awaitでのエラーをキャッチするのにも使う
-
then/catch の catch
- Promise専用の「約束が失敗したとき」(rejectされたとき)に、その失敗を受け取るためのもの
エラーが起きたとき
エラーの話になったので起きたらどうなるのってお話をするよ。
エラーが起きたら、どこで起きたか知って、どう対処するか選ぶだけ。
Promise さん専用の失敗の受け取り方(then/catch)
- Promise は成功(resolve)か失敗(reject)
- reject された失敗は .catch() で受け取る。
async/await の場合 (try/catch)
- await は Promise の reject を throw に変換
- throw されたエラーは try/catch で受け取れる。
| try/catch | then/catch | |
|---|---|---|
| どんなとき? | エラー全般 | Promiseを使うときに |
| 使える場所 | 普通の処理も、Promiseも | Promise(約束)を使うときだけ |
| 何をキャッチする? | エラーや例外全体(throw等) | Promiseの「失敗(reject)」 |
| 主な使い道・例 | 非同期も同期もどちらもOK | Promiseチェーン・非同期の分岐 |
| 書き方の違い | try { await ... } catch {...} | fetchData().then(...).catch(...) |
どちらも「キャッチする」だけど、エラー(例外)なのか、約束(Promise)の失敗なのかで使い方が違うよ!
setTimeoutとpromise
setTimeout を重ねる(ネストで使う)と順番の管理がむずかしくなるから、Promiseを使うと「順番どおりに実行する」「終わったら次へ進む」が分かりやすく書ける(可読性がよくなる)んだ。
setTimeoutのネストの例: NG
setTimeout(() => {
console.log("1つ目のデータをゲット!");
setTimeout(() => {
console.log("2つ目のデータをゲット!");
setTimeout(() => {
console.log("3つ目のデータをゲット!");
// さらに追加する場合はここにまたsetTimeoutを書いていく
}, 1000);
}, 1000);
}, 1000);
setTimeoutのネストの例: async/await
function wait(ms, message, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject("エラーになっちゃった…");
} else {
resolve(message);
}
}, ms);
});
}
async function main() {
try {
const result1 = await wait(1000, "1つ目のデータをゲット!");
console.log(result1);
const result2 = await wait(1000, "2つ目のデータをゲット!");
console.log(result2);
const result3 = await wait(1000, "3つ目のデータをゲット!");
console.log(result3);
} catch(error) {
console.log("エラー:", error);
}
}
main();
setTimeoutのネストの例: then/catch
function wait(ms, message, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject("エラーになっちゃった…");
} else {
resolve(message);
}
}, ms);
});
}
wait(1000, "1つ目のデータをゲット!")
.then(result1 => {
console.log(result1); // 1秒後
return wait(1000, "2つ目のデータをゲット!");
})
.then(result2 => {
console.log(result2); // さらに1秒後
return wait(1000, "3つ目のデータをゲット!");
})
.then(result3 => {
console.log(result3); // さらに1秒後
})
.catch(error => {
// どこかでreject(エラー)が起きたときだけここ
console.log("エラー:", error);
});
Q1. Promiseはいつ使うの?
「外部の処理に時間がかかるとき」や、「このAPIは結果をPromiseで返すよ」と決まっているとき。
| どんなとき? | Promise/asyncを使うと便利 | 使わなくてもいいとき |
|---|---|---|
| たくさんのお願いを順番にやりたい(複数の非同期→順番制御) | ✅ 約束した順番に進める | 1回だけなら順番を気にしなくてOK |
| お願いが終わったら次の行動をしたい(終了後に追加処理したい) | ✅ 終わってから次の約束ができる | すぐ終わるならそのままでOK |
| お願いがダメだったらやり直したい(エラー制御したい) | ✅ 失敗したこともすぐわかって、お知らせや対応が書けるよ | 失敗しても困らないなら気にしなくてOK |
Q2. if や for の中で Promise が使われてることがあるけど、どういうときに使うの?
if や for 自体はただの同期処理で、本来ならPromiseを使う必要はないよ!
ただし、その中で時間がかかる処理をしたいときに、中に 待つべき処理 があるから await を使うというだけ。
例(forの中で非同期処理):
const fetchData = (id) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`データ${id}`);
}, 1000);
});
};
async function run() {
for (let i = 0; i < 3; i++) {
const data = await fetchData(i);
console.log(data); // 毎秒1つずつ表示される
}
}
Q3. Promiseと async/await, try/catch の関係は?
ぜんぶ仲間!
-
Promise:時間がかかる処理を表す「箱」📦 -
async/await:Promiseを「すぐ終わる処理みたいに書き換えられる」仕組み🪄 -
try/catch:エラーが起きたときに止まらないようにする守りの受け止めネット🛡️
async/await(エイシンク・アウェイト)
async/await は Promise を「順番に見える書き方」にしただけで、中身は Promise と同じ仕組みだよ。
これも非同期だけど、もっと読みやすく書く方法!
やってることはPromiseと同じだよ!
awaitの後ろに書く「非同期関数」はPromiseを返すもの
-
awaitは、「この Promise の結果が届くまで、ここでいったん休むね」 という動きをする - Promise は“あとで必ず結果を連絡してくれる箱”だから、await はその連絡が来るまでストップしてくれるんだ。
連絡してくれるタイプの関数
-
fetch()(ネットのデータが返ってくるまで時間がかかる) -
async functionで作った関数(自動で Promise を返す) - setTimeout を Promise に包んだやつ(wait関数など)
基本構文
ここからは“小学生にもわかる”をいったん離れて実際のコードの書き方をまとめていくよ!
【例1】Promise を “その場で自作して” await する
「Promise って何者?」を体験するための最小構成。
const fetchData = async () => {
let result;
try {
result = await new Promise((resolve) => {
setTimeout(() => {
resolve("データ来た!");
}, 1000);
});
} catch (error) {
console.error("エラー:", error); // 失敗したときの処理
}
console.log(result); // 1秒後に「データ来た!」と出る
}
fetchData();
これ毎回書くのだるい → 関数化しよう →例2
【例2】 async/awaitで「待ちたい処理」を関数化して使いやすくした例
「待つ処理」→「データ来た!」を同期っぽく書ける。
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function main() {
await wait(1000); // 1秒待てる
console.log("データ来た!");
}
main();
【例3】ネスト地獄(コールバック・地層構造)はasync/awaitで劇的改善できる例
befote(可読性悪い)
setTimeout(() => {
console.log("1層目");
setTimeout(() => {
console.log("2層目");
setTimeout(() => {
console.log("3層目");
setTimeout(() => {
console.log("4層目");
setTimeout(() => {
console.log("5層目");
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
after
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function run() {
await wait(1000);
console.log("1層目");
await wait(1000);
console.log("2層目");
await wait(1000);
console.log("3層目");
await wait(1000);
console.log("4層目");
await wait(1000);
console.log("5層目");
}
run();
setTimeout の階層構造を待つ」→「ログ」→「待つ」→「ログ」みたいに順番に書ける
【例4】fetchやAPI系はPromiseを返すのでnew Promiseを書く必要なし
async function test() {
const response = await fetch("https://api.example.com/data");
// 必要ならここでresponseの処理を続ける
}
test();
ポイント:
-
async→ async をつけると、return の値を自動で Promise に包んでくれるんだ。 -
await→ 「この処理が終わるまで待ってて!」って命令
同じことをやってるコードで比較してみよう
Promiseだけで書くとこんな感じ
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("データが届きました!");
}, 1000);
});
}
const promise = fetchData();
// この段階では「箱を作っただけ」。
// 中身("データが届きました!")は 1秒後に入るけど、ここではまだ開けてないよ。
// あとでthenやawaitで開けて中身を使う
async/await+try/catchで書くとこうなる
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("データが届きました!");
}, 1000);
});
}
async function main() {
try {
const result = await fetchData();
console.log("成功:", result);
} catch (error) {
console.log("エラー:", error);
}
}
main();
then/catchで書くとこうなる
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("データが届きました!");
// reject("エラーが発生しました"); // ←エラーにしたい場合はこちらを使う
}, 1000);
});
}
fetchData()
.then((result) => {
console.log("成功:", result);
})
.catch((error) => {
console.log("エラー:", error);
});
※then/catch は「Promiseの標準的な書き方」で、async/await ほど読みやすくはないけど、挙動は同じだよ。
try / catch(トライ・キャッチ)
失敗するかもしれない処理を安全に試すときに使う。エラーが起きたときに止まらないようにする守りの魔法
ポイント💡:同期と非同期では try/catch の使われ方が少し違うよ。
非同期の仕事は「あとで結果を連絡する仕組み(Promise)」の中で動く。
そこでエラーが起きても、箱が「失敗したよ!」って伝えてくれるだけで、プログラムが止まったりはしないよ。
ただし、その失敗は .catch や try/catch で受け取ってあげる必要があるよ。
-
Promise+.then()のとcatch()がtry/catchの代わりになる
fetchData()
.then((result) => {
console.log("成功:", result);
})
.catch((error) => {
console.log("エラー:", error); // ← ここが catch の代わり
});
-
async/awaitを使うとき:
try {
const result = await fetchData();
console.log("成功:", result);
} catch (error) {
console.log("エラー:", error);
}
try/catchを「したほうがいいとき」と「しなくてもいいとき」
try/catchしたほうがいいとき
-
エラー内容を自分でコントロールしたいとき
- 例えば、ユーザーに「もう一度やり直してください」と伝えたい場合や、エラーの内容を分かりやすく返したい場合。
- エラーをログ(記録)を残したいとき
-
エラーが起きてもプログラムを止めたくないとき
- 例えば、複数の処理を続けて実行したい場合、1つ失敗しても他を続けたいとき。
-
APIのレスポンスをカスタマイズしたいとき
- 例えば、500エラーではなく400エラーや独自のエラーメッセージを返したいとき。
try/catchしなくても良いとき
-
フレームワークやランタイムが自動でエラー処理してくれるとき
- たとえば、HonoやExpressなどのWebフレームワークは、ルートハンドラでエラーが発生した場合、自動で500エラーなどのレスポンスを返してくれる仕組みがあるため、最低限のエラー処理はtry/catchなしでも動く。
-
テストコードの場合
- テストフレームワーク(VitestやJestなど)は、テスト内でエラーが起きたらそれを「テスト失敗」として自動で扱ってくれるので、通常はtry/catchを入れなくてもいい。
-
一番外側(グローバル)でまとめてエラー処理している場合
- 全体をまとめてエラーハンドリングする仕組みがある場合は、個々の処理でtry/catchしなくても良いことがある。
まとめ
同期・非同期・Promise・async/await・イベントループは、ぜんぶ「時間がかかる処理をどこで待つか」の話。
時間がかかるほうは Promise に入れてあとで返してもらい、async/await で待つだけ。
仕組みさえつながれば、“なぜあの順番になるの?”の理解に役立つ。