JavaScript 非同期プログラミング
JavaScript はシングルスレッドです。一度に1つのことしかできません。しかし、ネットワークリクエストに3秒かかる場合、ページが3秒間フリーズするのは困りますよね?非同期プログラミングがこの問題を解決します。待つ代わりに番号札を受け取り、準備ができたら戻ってきます。レストランの行列システムのようなもので、キッチンのドアの前で立つのではなく、座って番号が呼ばれるのを待ちます。
同期 vs 非同期
同期:コードは行ごとに実行されます。次の行は現在の行が完了するまで待ちます。列に並ぶようなもので、前の人が終わるまで待ちます。
非同期:特定の操作(ネットワークリクエスト、タイマー、ファイル I/O)は後続のコードをブロックせずに開始されます。完了時にコールバックや Promise で通知を受けます。
<script>
console.log('1');
setTimeout(function() {
console.log('2');
}, 1000);
console.log('3');
</script>
出力順は 1 → 3 → 2 です。setTimeout は非同期で、そのコールバックは1秒後に console.log('3') をブロックせずに実行されます。
コールバック
コールバックは最も基本的な非同期パターンです。関数を引数として渡し、非同期操作が完了した時に呼び出されます。
例:基本的なコールバックの使用
<div id="output" style="padding: 10px; border: 1px solid #ccc;"></div>
<script>
const output = document.getElementById('output');
function fetchData(callback) {
output.textContent = '読み込み中...';
setTimeout(function() {
callback('データ読み込み完了!');
}, 2000);
}
fetchData(function(data) {
output.textContent = data;
});
</script>
コールバック地獄
非同期操作が順番に依存し合うと、コールバックが深くネストされます。これが悪名高い「コールバック地獄」です。
<script>
getUser(function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
getItems(detail.itemId, function(items) {
console.log('4レベルの深さ。さらに複雑になると読みにくくなる');
});
});
});
});
</script>
Promise
Promise は非同期操作の最終結果を表すオブジェクトです。3つの状態があります。
| 状態 | 説明 |
|---|---|
pending |
進行中。結果はまだない |
fulfilled |
正常に完了。結果が利用可能 |
rejected |
失敗。エラーが利用可能 |
Promise が pending から fulfilled または rejected に遷移すると、二度と元に戻りません。射た矢は召回できないようなものです。
Promise の作成
<script>
const promise = new Promise(function(resolve, reject) {
// 非同期操作
// 成功時は resolve(result) を呼び出す
// 失敗時は reject(error) を呼び出す
});
</script>
then / catch / finally
<script>
promise
.then(function(result) { console.log(result); }) // 成功時に実行
.catch(function(error) { console.error(error); }) // 失敗時に実行
.finally(function() { console.log('完了'); }); // 常に実行
</script>
例:Promise でリクエストをシミュレート
<button id="loadBtn">データ読み込み</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');
function fetchUser() {
return new Promise(function(resolve, reject) {
output.textContent = '読み込み中...';
setTimeout(function() {
const success = true;
if (success) {
resolve({ name: 'Alice', age: 25 });
} else {
reject(new Error('読み込み失敗'));
}
}, 1500);
});
}
btn.addEventListener('click', function() {
fetchUser()
.then(function(user) {
output.textContent = '名前: ' + user.name + ', 年齢: ' + user.age;
})
.catch(function(err) {
output.textContent = 'エラー: ' + err.message;
})
.finally(function() {
console.log('リクエスト完了(成功または失敗)');
});
});
</script>
チェーン
then 自体が Promise を返すため、呼び出しをチェーンできます。コールバック地獄が直線になります。
<script>
fetchUser()
.then(function(user) { return fetchOrders(user.id); })
.then(function(orders) { return fetchDetail(orders[0].id); })
.then(function(detail) { console.log(detail); })
.catch(function(err) { console.error(err); });
</script>
catch を置くと、任意のステップのエラーをキャッチします。各 then の後に catch を置く必要はありません。
async / await
async/await は Promise のシンタシュガーです。非同期コードを同期的に見せかけ、可読性を大幅に向上させます。
async:自動的に Promise を返す非同期関数を宣言await:Promise が解決するまで実行を一時停止し、結果を返す
例:上記のリクエストを async/await で書き直す
<button id="loadBtn">データ読み込み</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');
function fetchUser() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ name: 'Bob', age: 30 });
}, 1500);
});
}
btn.addEventListener('click', async function() {
try {
output.textContent = '読み込み中...';
const user = await fetchUser();
output.textContent = '名前: ' + user.name + ', 年齢: ' + user.age;
} catch (err) {
output.textContent = 'エラー: ' + err.message;
} finally {
console.log('リクエスト完了');
}
});
</script>
async/await は「番号札をもらって待つ」を「並んでそのまま待つ」に変えます。コードフローが直線的になり、可読性が大幅に向上します。
エラー処理
async/await は try/catch でエラー処理を行います。同期コードと同じパターンです。
<script>
async function loadAll() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
console.log(orders);
} catch (err) {
console.error('何か問題がありました:', err);
}
}
</script>
Promise.all と Promise.race
| メソッド | 説明 |
|---|---|
Promise.all([p1, p2, p3]) |
すべてが成功した場合のみ成功。1つが失敗すると即座に失敗 |
Promise.race([p1, p2, p3]) |
最初に完了したもの(成功または失敗)の結果を使用 |
例:Promise.all による並行リクエスト
<button id="loadAll">並行読み込み</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadAll');
const output = document.getElementById('output');
function delay(ms, value) {
return new Promise(function(resolve) {
setTimeout(function() { resolve(value); }, ms);
});
}
btn.addEventListener('click', async function() {
output.textContent = '読み込み中...';
const results = await Promise.all([
delay(1000, 'ユーザーデータ'),
delay(1500, '注文データ'),
delay(800, '設定データ')
]);
output.textContent = 'すべて読み込み完了:\n' + results.join(' | ');
});
const raceBtn = document.createElement('button');
raceBtn.textContent = 'レース読み込み';
document.body.appendChild(raceBtn);
raceBtn.addEventListener('click', async function() {
const fastest = await Promise.race([
delay(1000, 'ソース A(1秒)'),
delay(500, 'ソース B(0.5秒)'),
delay(800, 'ソース C(0.8秒)')
]);
output.textContent = '最速: ' + fastest;
});
</script>
Promise.all はグループディナーのようなもの。全員が到着するまで食べられません。Promise.race はレースのようなもの。最初にゴールした人が勝ちです。独立したリクエストの高速化には all を、複数のミラーソースから最速のものを得るには race を使いましょう。
📖 まとめ
- 同期コードは後続の実行をブロックするが、非同期コードはブロックしない。シングルスレッドでも「複数のことができる」
- コールバックは最も基本的な非同期パターン。深いネストはコールバック地獄を生む
- Promise には3つの状態:
pending → fulfilledまたはpending → rejected。状態遷移は不可逆 then/catch/finallyは Promise を消費する。チェーンはコールバック地獄を解決async/awaitは Promise のシンタシュガー。awaitはasync関数内でのみ使用可能Promise.allはすべての成功を待つ。Promise.raceは最初に完了したものを取る
❓ よくある質問
await は使えますか?await は async 関数内でのみ使用できます。通常の関数で await を使うと構文エラーになります。トップレベルの await はモジュールでサポートされていますが、ブラウザのサポート状況は異なります。Promise.all のいずれかの Promise が失敗するとどうなりますか?Promise.all は最初のエラーで即座に拒否されます。他の Promise はまだ実行されますが、その結果は無視されます。「結果に関係なくすべて完了する」必要がある場合は、Promise.allSettled を使いましょう。async 関数は何を返しますか?async 関数は常に Promise を返します。return 42 としても、実際には Promise.resolve(42) を返します。そのため、.then() で結果を消費できます。📝 演習
- 基礎:
Promise+setTimeoutを使って2秒の遅延をシミュレートし、成功時に「完了」と出力してください。 - 中級:上記を
async/awaitで書き直し、try/catchのエラー処理を追加してください。 - チャレンジ:
Promise.allを使って、異なる遅延(1秒/2秒/3秒)の3つのシミュレートリクエストを同時に発火し、すべて完了した時の合計経過時間を計算して表示してください。



