非同期処理とは何か?

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チェーンの煩雑さも解消されます。コードが「上から下へ」読めるのが大きな利点です。

💡 awaitasync 関数の中でしか使えません(トップレベル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を使う
  • forEachawaitは相性が悪いのでfor...ofPromise.allを使う

非同期処理はJavaScriptの最重要スキルのひとつです。最初は混乱しやすいですが、Promise → async/await → 並列処理の順で段階的に理解していけば、必ず使いこなせるようになります。