非同期処理とは何か?
JavaScriptは基本的にシングルスレッドで動作する言語です。つまり、一度に1つの処理しか実行できません。しかし、サーバーへのリクエストやファイル読み込みのような時間がかかる処理を「待つ」あいだ、画面を完全に止めてしまっては困ります。
そこで登場するのが非同期処理(asynchronous processing)です。非同期処理を使うと、時間のかかる処理を「バックグラウンド」で進めながら、他のコードを実行できます。
コールバック地獄からの脱却
非同期処理のいちばん古い書き方はコールバック関数でした。しかしネストが深くなると、いわゆる「コールバック地獄」になります:
// コールバック地獄の例
getUser(userId, function(user) { getPosts(user.id, function(posts) { getComments(posts[0].id, function(comments) { console.log(comments); // さらに続くと地獄に... }); });
});
この問題を解決するために登場したのが Promise、そしてそれをさらに読みやすくした async/await です。
Promiseの基礎
Promiseは「将来のいつか結果が返ってくる約束」を表すオブジェクトです。Promiseは3つの状態を持ちます:
- pending(待機中):処理がまだ完了していない
- fulfilled(成功):処理が成功して結果が返ってきた
- rejected(失敗):処理が失敗してエラーが返ってきた
Promiseの作り方
// Promiseを返す関数
function fetchUserName(userId) { return new Promise((resolve, reject) => { setTimeout(() => { if (userId > 0) { resolve(`ユーザー${userId}`); } else { reject(new Error("無効なユーザーID")); } }, 1000); });
} // .then()で結果を受け取る
fetchUserName(1) .then(name => console.log(name)) // "ユーザー1" .catch(err => console.error(err)); Promiseチェーン
.then()はPromiseを返すので、続けてつなげられます:
fetchUserName(1) .then(name => { console.log(name); return fetchPosts(name); // 次のPromiseを返す }) .then(posts => { console.log(posts); }) .catch(err => console.error(err)); async/await の登場
Promiseは便利ですが、.then()のチェーンが長くなると読みにくくなります。async/awaitを使うと、非同期処理を同期処理のように書けるようになります。
基本構文
// asyncをつけた関数の中でawaitが使える
async function showUserPosts(userId) { const name = await fetchUserName(userId); console.log(name); const posts = await fetchPosts(name); console.log(posts);
} showUserPosts(1); たったこれだけで、コールバック地獄もPromiseチェーンの煩雑さも解消されます。コードが「上から下へ」読めるのが大きな利点です。
awaitは async 関数の中でしか使えません(トップレベルawaitを除く)。普通の関数で使うとシンタックスエラーになります。asyncが返すのは常にPromise
async関数はreturnした値を自動的にPromiseでラップして返します:
async function getNumber() { return 42; // 数値をreturn
} // 使う側はPromiseとして扱う
getNumber().then(n => console.log(n)); // 42 // または別のasync関数の中でawait
async function main() { const n = await getNumber(); console.log(n); // 42
} エラーハンドリング
async/awaitでは try...catch でエラーを捕捉できます。これは同期処理と同じ書き方です:
async function safeFetch(userId) { try { const name = await fetchUserName(userId); const posts = await fetchPosts(name); return posts; } catch (err) { console.error("エラーが発生しました:", err.message); return []; // デフォルト値を返す } finally { console.log("処理完了"); }
} return fetchPosts(name) のように書くと、その関数の中の例外はtry/catchをすり抜けます。エラーを捕捉したい場合は return await fetchPosts(name) としましょう。並列処理:Promise.all
独立した複数の非同期処理を 並行して 実行したい場合、await を順番に書くと逐次実行になり遅くなります:
// 遅い書き方(合計3秒)
async function slow() { const a = await fetch1(); // 1秒 const b = await fetch2(); // 1秒 const c = await fetch3(); // 1秒
} // 速い書き方(合計1秒)
async function fast() { const [a, b, c] = await Promise.all([ fetch1(), fetch2(), fetch3() ]);
} Promise.allは配列内のすべてのPromiseが成功したら結果を配列で返し、ひとつでも失敗するとrejectされます。
失敗を許容する Promise.allSettled
「ひとつ失敗しても他は完了させたい」場合はPromise.allSettledを使います:
const results = await Promise.allSettled([ fetch1(), fetch2(), fetch3()
]); results.forEach((result, i) => { if (result.status === "fulfilled") { console.log(`${i}: 成功`, result.value); } else { console.log(`${i}: 失敗`, result.reason); }
}); 実践例:APIからデータ取得
fetch APIを使った典型的な非同期処理のパターンです:
async function getUserProfile(userId) { try { const response = await fetch(`/api/users/${userId}`); if (!response.ok) { throw new Error(`HTTPエラー: ${response.status}`); } const data = await response.json(); return data; } catch (err) { console.error("取得失敗:", err); throw err; // 上位に伝搬 }
} // 呼び出し側
async function showProfile() { const profile = await getUserProfile(123); document.getElementById("name").textContent = profile.name;
} よくある落とし穴
1. forEachの中でawaitしても順次実行されない
// ❌ 期待通りに動かない
[1, 2, 3].forEach(async (id) => { await processItem(id); // 並行に走り、順序保証もなし
}); // ✅ for...of を使う(順次実行)
for (const id of [1, 2, 3]) { await processItem(id);
} // ✅ 並列に走らせたいなら Promise.all
await Promise.all([1, 2, 3].map(processItem)); 2. awaitを忘れる
// ❌ awaitなし → Promiseオブジェクトがそのまま代入される
const data = fetch("/api/data");
console.log(data); // Promise { ... } と表示される // ✅ awaitを忘れずに
const data = await fetch("/api/data").then(r => r.json()); 3. 直列処理になっているのに気づかない
独立した処理を await で順番に書くと、不必要に直列実行になり遅くなります。並行できる処理はPromise.allでまとめましょう。
まとめ
JavaScript の非同期処理を扱うために覚えておきたいポイント:
- Promiseは「未来の結果」を表すオブジェクト
- async/awaitを使えば非同期処理を同期処理のように読みやすく書ける
- エラーはtry...catchで捕捉する
- 並行実行できる処理はPromise.allでまとめてパフォーマンスを上げる
- 失敗を許容したい場合はPromise.allSettledを使う
forEachとawaitは相性が悪いのでfor...ofかPromise.allを使う
非同期処理はJavaScriptの最重要スキルのひとつです。最初は混乱しやすいですが、Promise → async/await → 並列処理の順で段階的に理解していけば、必ず使いこなせるようになります。