まず「非同期コールバック」を一言でイメージする
非同期コールバックは、
「“今すぐ”ではなく、“あとで”呼び出してもらうために登録しておく関数」
のことです。
普通のコールバックも「あとで呼ばれる関数」ですが、
非同期コールバックは特に、
- いつ呼ばれるかが「今この瞬間」ではない
- しかも「今の処理が終わった“後のタイミング”で、イベントループによって呼ばれる」
という特徴があります。
ここが重要です。
単に「関数を引数に渡しているからコールバック」ではなく、
「イベントループによって“別のタイミングで”実行されるコールバック」が、非同期コールバック です。
まずは普通のコールバックと比べてみる
同期コールバック(今すぐ呼ばれるパターン)
これは「同期コールバック」の例です。
function runCallback(callback) {
console.log("runCallback start");
callback(); // ここで即呼ばれる
console.log("runCallback end");
}
runCallback(() => {
console.log("中で実行されるコールバック");
});
JavaScript実行結果はこうなります。
runCallback start
中で実行されるコールバック
runCallback end
runCallback の中で callback() を呼んだ瞬間に実行されているので、
「呼び出しの流れの中で、その場で発動する」タイプです。
これは「同期コールバック」とよく呼ばれます。
非同期コールバック(“あとで”呼ばれるパターン)
次は非同期コールバックの例です。
console.log("A: start");
setTimeout(() => {
console.log("B: timeout callback");
}, 1000);
console.log("C: end");
JavaScript実行結果は、
A: start
C: end
(1秒後)
B: timeout callback
です。
setTimeout に渡したコールバックは、setTimeout の中では「今すぐ実行されません」。
Web API に「あとでこの関数を実行して」と登録され、
時間が経ったあとにイベントループを通じて呼び出されます。
ここが重要です。
「関数を渡して、その場で呼ぶ」なら同期コールバック。
「関数を渡して、“あとで”呼んでもらう」なら非同期コールバック。
この違いをまずしっかり分けてイメージしてください。
非同期コールバックが出てくる典型パターン
setTimeout / setInterval
タイマー系は非同期コールバックの教科書のような例です。
setTimeout(() => {
console.log("1秒後に実行される非同期コールバック");
}, 1000);
JavaScriptここでは、
- コールバック関数
() => { ... }をsetTimeoutに渡す - Web API が 1000ms 計測する
- 経過後、「このコールバックをタスクキューに入れて」とイベントループに渡す
- イベントループがスタックが空いたタイミングでそれを実行する
という流れになります。
setInterval も同じで、「◯ミリ秒ごとにこの非同期コールバックをタスクキューに投げてください」という仕組みです。
DOM イベント(クリックなど)
ユーザーの操作も、非同期コールバックの典型です。
const button = document.querySelector("#btn");
button.addEventListener("click", () => {
console.log("クリック時の非同期コールバック");
});
JavaScriptここで渡している関数も、非同期コールバックです。
- コードを実行した時点では呼ばれない
- ユーザーが「いつか」クリックした瞬間に
- ブラウザ(Web API)がイベントを検知し、「登録されていたコールバック」をタスクキューに入れる
- イベントループがそれを実行する
という流れなので、「いつ実行されるかは未来のどこか」です。
非同期通信(昔ながらのコールバックスタイル)
Promise や fetch が出る前は、通信も非同期コールバックで扱っていました。
function getData(callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/data.json");
xhr.onload = function () {
callback(xhr.responseText);
};
xhr.send();
}
console.log("リクエスト開始");
getData((data) => {
console.log("非同期コールバックで受け取ったデータ:", data);
});
console.log("他の処理");
JavaScriptここで getData に渡している匿名関数も非同期コールバックです。
実際に呼ばれるのは、
- ネットワーク通信が完了し
- onload イベントが発火され
- ブラウザがそのイベントハンドラをタスクキューに入れ
- イベントループがそれを実行したタイミング
です。
なぜ非同期コールバックが必要になるのか
「終わるまで待ち続けると、全部止まる」から
もし非同期処理を同期で書いてしまうと、
時間のかかる処理のあいだ、JavaScript のメインスレッドが完全に止まってしまいます。
例えば 3 秒待ちを同期でやると:
console.log("A: start");
const start = Date.now();
while (Date.now() - start < 3000) {
// 3秒間止まり続ける
}
console.log("B: end");
JavaScriptこの 3 秒間、UI は固まり、クリックもスクロールも反応しません。
非同期コールバックを使えば、同じ「3秒後に処理したい」をこう書けます。
console.log("A: start");
setTimeout(() => {
console.log("B: 3秒後の非同期コールバック");
}, 3000);
console.log("C: 他の処理を続けられる");
JavaScript3 秒待っている間も、他の処理やユーザー操作に反応できます。
ここが重要です。
「待っている間も別のことをできるようにする」ために、
「結果が出たときに呼ばれる非同期コールバック」というスタイルが必要になる。
JavaScript がシングルスレッドだからこそ
JavaScript は基本的に「1 本のメインスレッド」で動いています。
- この 1 本が UI イベントも画面更新も全部担当している
- そこを長時間占有する同期処理を置くと、全部止まる
という構造なので、「時間のかかる処理を外に追い出す」しかありません。
非同期コールバックは、
- 待つ仕事(タイマー・通信・イベント監視)は Web API に任せる
- 結果が出たときに実行したい処理だけ、コールバックとして登録しておく
という分業の真ん中にいる存在です。
非同期コールバックの「実行タイミング」をもう少し深掘りする
「終わった瞬間」ではなく「キューに入ってから」という一呼吸
非同期コールバックは、
「条件が満たされた瞬間に即実行」されるわけではありません。
流れとしては、
条件が満たされる(時間が経つ・クリックされる・通信が終わる)
→ Web API が「このコールバックをタスクキューに入れる」
→ イベントループが「コールスタックが空いたタイミングで」それを実行する
という三段階です。
例えば:
console.log("A");
setTimeout(() => {
console.log("B: timeout");
}, 0);
console.log("C");
JavaScript0 ミリ秒でも、
A
C
B: timeout
という順になります。
理由は、
- グローバルコード(A → setTimeout → C)が終わるまでは、コールスタックが埋まっている
- 0ms 経過後に、コールバックはタスクキューに「準備完了」として待機する
- グローバルコードが終わってスタックが空になったあと、ようやくイベントループがそのコールバックを実行する
からです。
ここが重要です。
非同期コールバックは「条件成立 → 即実行」ではなく、「条件成立 → キューに積む → スタックが空いたら実行」というワンクッション付きで動いている。
Promise.then や async/await との関係(軽く触れる)
今の主流は Promise / async/await ですが、
中身ではやっぱり「非同期コールバック」が動いています。
Promise.resolve().then(() => {
console.log("これはマイクロタスクの非同期コールバック");
});
JavaScriptこの then の中身も、「Promise が解決されたときに実行される非同期コールバック」です。
違いは、
- タスクキューではなく「マイクロタスクキュー」に積まれる
- 通常タスクよりも優先度高く実行される
という点だけで、「あとで実行されるコールバックである」という本質は同じです。
非同期コールバックの書きにくさと「コールバック地獄」
ネストが深くなる問題
非同期処理をコールバックだけで書いていくと、すぐこうなります。
getData((data1) => {
processData1(data1, (result1) => {
getMoreData(result1, (data2) => {
processData2(data2, (result2) => {
console.log("全部終わった:", result2);
});
});
});
});
JavaScriptどの ) がどこに対応してるか分かりにくく、
エラー処理や条件分岐が入るとすぐに崩壊します。
これが「コールバック地獄」と呼ばれる状態です。
この問題を解決するために、Promise や async/await が生まれました。
でも、非同期コールバックそのものが消えたわけではなく、
Promise も中で「非同期コールバック(then など)」を利用しています。
ここが重要です。
「書きやすさ」のために Promise や async/await を使うが、
頭の中では「結局、非同期コールバックがイベントループ経由で呼ばれている」と理解しておくと強い。
まとめ:非同期コールバックとは何かを言い直す
非同期コールバックを一言でまとめると、
「時間がかかったり、いつ起こるか分からない処理(タイマー・通信・イベントなど)が終わった“あとで”、イベントループによって呼び出されるよう登録された関数」
です。
押さえておきたいポイントを整理すると、次のようになります。
「場で即呼ばれる」ものではなく、「あとで呼び返してもらうために渡す関数」であること。
setTimeout / setInterval / addEventListener / 古い通信 API などで、第1引数・第2引数に渡している関数が典型的な非同期コールバックであること。
実際の呼び出しは、「条件が満たされた瞬間」ではなく、「タスクキューやマイクロタスクキューに積まれ、コールスタックが空いたタイミング」で行われること。
JavaScript がシングルスレッドである以上、「待ち時間のあいだも他の処理を続ける」ための基本的な仕組みとして非同期コールバックが必要であること。
Promise / async/await も、表現が変わっただけで中身には「非同期コールバック」の考え方が流れていること。
最初のステップとしては、
setTimeout
addEventListener
簡単な通信処理(ダミーでも OK)
あたりを使って、「これは非同期コールバックだ」と意識しながらコードを書いてみてください。
「どのタイミングで登録されて、どのタイミングで実行されるのか」を
イベントループやタスクキューのイメージと一緒に追いかけていくと、
非同期コールバックは「ただの難しい言葉」ではなく、
「JavaScript が止まらないための当たり前の道具」として感じられるようになります。
