基礎

JavaScript Promise入門|非同期処理を理解する

JavaScript Promise 非同期

JavaScript Promise入門
非同期処理を理解する

JavaScriptのPromiseの仕組みを基礎から解説。コールバック地獄の解決、Promiseチェーン、Promise.all、async/awaitまでステップごとに学べます。

こんな人向けの記事です

  • JavaScriptの非同期処理を基礎から理解したい
  • Promiseの仕組みを正確に知りたい
  • async/awaitを使いこなしたい

Step 1非同期処理とは(同期 vs 非同期)

JavaScriptはシングルスレッドの言語です。つまり、一度に1つの処理しか実行できません。しかし、API通信やファイル読み込みなど時間のかかる処理を待っている間、画面が固まってしまっては困ります。そこで「非同期処理」が必要になります。

同期処理と非同期処理の違い

同期処理:上から順番に実行し、1つの処理が終わるまで次に進まない。
非同期処理:時間のかかる処理を「後で結果を受け取る」形にして、その間に別の処理を進める。

JavaScript(同期処理の例)
// 同期処理:上から順番に実行される
console.log("1. 開始");
console.log("2. 処理中...");
console.log("3. 完了");
// 出力順: 1 → 2 → 3
JavaScript(非同期処理の例)
// 非同期処理:setTimeoutは後回しにされる
console.log("1. 開始");

setTimeout(() => {
  console.log("2. 非同期処理完了");
}, 1000);

console.log("3. 次の処理");
// 出力順: 1 → 3 → 2(2は1秒後に出力される)

setTimeout はブラウザに「1秒後にこの関数を実行して」と依頼するだけで、JavaScript自体は次の行に進みます。これが非同期処理の基本的な動きです。

比較項目同期処理非同期処理
実行順序上から順番に1つずつ待たずに次の処理へ進む
ブロッキング前の処理が終わるまで止まる止まらない
代表例変数代入、計算API通信、タイマー、ファイル読み込み
結果の取得戻り値で即座に取得コールバック・Promise等で後から取得

Step 2コールバック地獄の問題

Promiseが登場する前、非同期処理の結果を受け取るにはコールバック関数を使うのが一般的でした。しかし、非同期処理が連鎖するとコードが右に深くネストされていく「コールバック地獄(Callback Hell)」が発生します。

JavaScript(コールバック地獄の例)
// ユーザー情報を取得 → 注文を取得 → 商品を取得...
getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetail(orders[0].id, function(detail) {
      getProduct(detail.productId, function(product) {
        console.log(product.name);
        // さらにネストが続く...
      }, function(err) {
        console.error("商品取得エラー:", err);
      });
    }, function(err) {
      console.error("注文詳細取得エラー:", err);
    });
  }, function(err) {
    console.error("注文一覧取得エラー:", err);
  });
}, function(err) {
  console.error("ユーザー取得エラー:", err);
});
コールバック地獄の問題点
  • 可読性が低い:ネストが深くなりコードが右に伸びていく
  • エラーハンドリングが煩雑:各コールバックごとにエラー処理が必要
  • 制御フローが複雑:処理の順番が追いにくい
  • 保守性が低い:変更・修正が困難

この問題を根本的に解決するために導入されたのが Promise です。ES2015(ES6)で正式に仕様に追加され、非同期処理をフラットに書けるようになりました。

Step 3Promiseの基本(new Promise, then, catch, finally)

Promiseは非同期処理の「最終的な結果」を表すオブジェクトです。Promiseは以下の3つの状態のいずれかを持ちます。

状態意味遷移条件
pending待機中(初期状態)作成直後
fulfilled成功(解決済み)resolve() が呼ばれた
rejected失敗(拒否済み)reject() が呼ばれた
状態遷移のルール

Promiseの状態は pending から fulfilled または rejected に一度だけ変わります。一度確定した状態は二度と変わりません。これを「settled(確定)」と呼びます。

JavaScript(Promiseの作成と使用)
// Promiseの作成
const myPromise = new Promise((resolve, reject) => {
  // 非同期処理をここに書く
  const success = true;

  if (success) {
    resolve("処理が成功しました");  // fulfilled状態にする
  } else {
    reject(new Error("処理が失敗しました"));  // rejected状態にする
  }
});

// Promiseの使用
myPromise
  .then((result) => {
    // fulfilledの時に実行される
    console.log(result);  // "処理が成功しました"
  })
  .catch((error) => {
    // rejectedの時に実行される
    console.error(error.message);
  })
  .finally(() => {
    // 成功・失敗に関わらず必ず実行される
    console.log("処理が完了しました");
  });

実用的な例として、APIからデータを取得する関数をPromiseで作ってみましょう。

JavaScript(API取得をPromiseで包む)
function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    // XMLHttpRequestによる通信(レガシーな方法)
    const xhr = new XMLHttpRequest();
    xhr.open("GET", `/api/users/${userId}`);

    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(`HTTP Error: ${xhr.status}`));
      }
    };

    xhr.onerror = () => reject(new Error("ネットワークエラー"));
    xhr.send();
  });
}

// 使用例
fetchUserData(1)
  .then(user => console.log(user.name))
  .catch(err => console.error("取得失敗:", err.message));
よくある間違い

new Promise のコールバック内で return を使っても意味がありません。必ず resolve() または reject() を呼んで状態を変更してください。また、resolve/reject を呼んだ後も処理は続行されるため、必要なら return で以降の処理を止めましょう。

Step 4Promiseチェーン

then() は新しいPromiseを返すため、.then().then().then() のようにチェーンできます。これにより、Step 2で見たコールバック地獄をフラットに書き直せます。

JavaScript(Promiseチェーンで書き直す)
// コールバック地獄だったコードをPromiseチェーンで書き直す
getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetail(orders[0].id))
  .then(detail => getProduct(detail.productId))
  .then(product => {
    console.log(product.name);
  })
  .catch(err => {
    // どの段階のエラーもここで一括キャッチ
    console.error("エラー:", err.message);
  });

ネストが消え、処理の流れが上から下に一直線になりました。エラーハンドリングも catch 1箇所で済みます。

thenの戻り値のルール

then のコールバックが返す値によって、次の then に渡される値が変わります。

  • 普通の値を返す:その値がそのまま次の then に渡される
  • Promiseを返す:そのPromiseが解決された値が次の then に渡される
  • 何も返さないundefined が次の then に渡される
JavaScript(チェーンでの値の変換)
// 値を変換しながらチェーンする
fetch("/api/users/1")
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();  // Promiseを返す
  })
  .then(user => {
    console.log(user.name);
    return user.name.toUpperCase();  // 普通の値を返す
  })
  .then(upperName => {
    console.log(upperName);  // 大文字に変換された名前
  })
  .catch(err => {
    console.error("エラー:", err.message);
  });
チェーンの切断に注意

then の中で return を忘れると、チェーンが「切れて」次の thenundefined が渡されます。非同期処理を含む場合は特に return を忘れないようにしましょう。

Step 5Promise.all / Promise.race / Promise.allSettled

複数の非同期処理を並行して実行したい場合に便利な静的メソッドがあります。

Promise.all:すべて成功したら結果をまとめる

JavaScript(Promise.all)
// 3つのAPIを同時に呼ぶ
const userPromise = fetch("/api/users/1").then(r => r.json());
const postsPromise = fetch("/api/posts?userId=1").then(r => r.json());
const settingsPromise = fetch("/api/settings").then(r => r.json());

Promise.all([userPromise, postsPromise, settingsPromise])
  .then(([user, posts, settings]) => {
    // 3つすべてが成功した時だけ実行される
    console.log("ユーザー:", user.name);
    console.log("投稿数:", posts.length);
    console.log("テーマ:", settings.theme);
  })
  .catch(err => {
    // 1つでも失敗したらここに来る
    console.error("いずれかのAPIが失敗:", err.message);
  });

Promise.race:最初に確定したものを採用

JavaScript(Promise.race でタイムアウト実装)
// タイムアウト付きfetch
function fetchWithTimeout(url, timeoutMs) {
  const fetchPromise = fetch(url);

  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error("タイムアウト")), timeoutMs);
  });

  // 先に確定した方が結果になる
  return Promise.race([fetchPromise, timeoutPromise]);
}

fetchWithTimeout("/api/slow-endpoint", 3000)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => console.error(err.message));  // "タイムアウト"

Promise.allSettled:全部の結果を取得(成功・失敗問わず)

JavaScript(Promise.allSettled)
const promises = [
  fetch("/api/users/1").then(r => r.json()),
  fetch("/api/users/999").then(r => {
    if (!r.ok) throw new Error("Not Found");
    return r.json();
  }),
  fetch("/api/users/2").then(r => r.json()),
];

Promise.allSettled(promises).then(results => {
  results.forEach((result, i) => {
    if (result.status === "fulfilled") {
      console.log(`#${i} 成功:`, result.value.name);
    } else {
      console.log(`#${i} 失敗:`, result.reason.message);
    }
  });
});
// #0 成功: Alice
// #1 失敗: Not Found
// #2 成功: Bob
メソッド挙動使いどころ
Promise.all全て成功で解決、1つでも失敗で拒否全てのデータが揃わないと表示できない場面
Promise.race最初に確定したものを採用タイムアウト実装、最速レスポンスの採用
Promise.allSettled全て確定するまで待ち、成功・失敗の結果を配列で返す一部失敗しても残りの結果が欲しい場面
Promise.any最初に成功したものを採用(全失敗で拒否)複数のミラーサーバーから最速で取得

Step 6async/awaitでPromiseをシンプルに書く

ES2017で導入された async/await は、Promiseを同期処理のように書ける構文です。Promiseチェーンよりさらに読みやすいコードになります。

JavaScript(Promiseチェーン vs async/await)
// Promiseチェーン版
function getUserData(userId) {
  return getUser(userId)
    .then(user => getOrders(user.id))
    .then(orders => getOrderDetail(orders[0].id))
    .then(detail => getProduct(detail.productId));
}

// async/await版(同じ処理)
async function getUserData(userId) {
  const user = await getUser(userId);
  const orders = await getOrders(user.id);
  const detail = await getOrderDetail(orders[0].id);
  const product = await getProduct(detail.productId);
  return product;
}
async/awaitの仕組み

async 関数は常にPromiseを返します。await はPromiseの解決を待ってから値を取り出します。内部的にはPromiseチェーンと同じ動きですが、構文が同期的になるため可読性が格段に上がります。

エラーハンドリング:try/catch

JavaScript(async/awaitのエラーハンドリング)
async function fetchUserProfile(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }

    const user = await response.json();
    console.log("ユーザー名:", user.name);
    return user;
  } catch (error) {
    console.error("取得失敗:", error.message);
    return null;  // デフォルト値を返す
  } finally {
    console.log("API呼び出し完了");
  }
}

並行実行:await + Promise.all

JavaScript(並行実行のパターン)
async function loadDashboard(userId) {
  // 悪い例:順番に実行される(遅い)
  // const user = await fetchUser(userId);
  // const posts = await fetchPosts(userId);
  // const notifications = await fetchNotifications(userId);

  // 良い例:並行に実行される(速い)
  const [user, posts, notifications] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchNotifications(userId),
  ]);

  return { user, posts, notifications };
}
awaitの逐次実行に注意

await を連続で書くと、前の処理が終わるまで次が始まりません。互いに依存しない処理は Promise.all で並行実行しましょう。3つのAPI呼び出しが各1秒かかる場合、逐次なら3秒、並行なら約1秒で済みます。

ループ内でのawait

JavaScript(ループでの非同期処理)
const userIds = [1, 2, 3, 4, 5];

// 逐次実行(順番に処理したい場合)
async function fetchUsersSequential(ids) {
  const users = [];
  for (const id of ids) {
    const user = await fetchUser(id);
    users.push(user);
  }
  return users;
}

// 並行実行(順番を気にしない場合)
async function fetchUsersParallel(ids) {
  const users = await Promise.all(
    ids.map(id => fetchUser(id))
  );
  return users;
}

この記事のまとめ

  • JavaScriptはシングルスレッドのため、非同期処理で待ち時間を有効活用する
  • コールバックの連鎖は「コールバック地獄」を引き起こす
  • Promiseは非同期処理の結果を表すオブジェクトで、then/catch/finally で扱う
  • Promiseチェーンによりネストをフラットに書ける
  • Promise.all で並行実行、Promise.race でタイムアウト、Promise.allSettled で全結果取得
  • async/await はPromiseを同期風に書ける構文で、try/catch でエラーを処理する
  • 独立した非同期処理は await Promise.all() で並行実行して高速化する