非同期処理とは何か?
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 → 並列処理の順で段階的に理解していけば、必ず使いこなせるようになります。