非同期処理とは何か?

JavaScriptは基本的にシングルスレッドで動作する言語です。つまり、一度に1つの処理しか実行できません。しかし、サーバーへのリクエストやファイル読み込みのような時間がかかる処理を「待つ」あいだ、画面を完全に止めてしまっては困ります。

そこで登場するのが非同期処理(asynchronous processing)です。非同期処理を使うと、時間のかかる処理を「バックグラウンド」で進めながら、他のコードを実行できます。

💡 同期処理は「順番に1つずつ」、非同期処理は「複数を並行して」進められると理解すると分かりやすいです。

コールバック地獄からの脱却

非同期処理のいちばん古い書き方はコールバック関数でした。しかしネストが深くなると、いわゆる「コールバック地獄」になります:

// コールバック地獄の例
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("処理完了"); }
}
⚠️ awaitしないとエラーが捕捉できません。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 → 並列処理の順で段階的に理解していけば、必ず使いこなせるようになります。