JavaScriptのPromise

2025-07-28

はじめに

前章までで、非同期処理の基本とコールバック関数について学びました。コールバック関数には「コールバック地獄」やエラー処理の難しさなどの問題があることを理解したと思います。この章では、これらの問題を解決するためにES6(ES2015)で導入された「Promise」について詳しく解説します。Promiseは現代JavaScriptの非同期処理の基盤となる重要な概念で、async/awaitの基礎にもなっています。

Promiseとは?

基本的な定義

Promise(プロミス)は、非同期処理の最終的な完了(または失敗)とその結果の値を表現するオブジェクトです。日本語で「約束」という意味通り、「将来のいつか値が取得できることを約束する」という概念です。

3つの状態

Promiseオブジェクトは以下の3つの状態のいずれかを取ります:

  1. pending(待機): 初期状態。履行も拒否もされていない
  2. fulfilled(履行): 処理が成功して完了した状態
  3. rejected(拒否): 処理が失敗した状態

一度fulfilledまたはrejectedになると、Promiseの状態はそれ以降変化しません(不変性)。

シンプルな例

const myPromise = new Promise((resolve, reject) => {
  // 非同期処理を実行
  setTimeout(() => {
    const success = true; // ここをfalseにするとrejectが呼ばれる
    if (success) {
      resolve("処理が成功しました!");
    } else {
      reject("エラーが発生しました");
    }
  }, 1000);
});

myPromise
  .then((result) => {
    console.log(result); // "処理が成功しました!"
  })
  .catch((error) => {
    console.error(error); // "エラーが発生しました"
  });

なぜPromiseが必要なのか?

コールバック関数の問題点

  1. コールバック地獄: ネストが深くなり、コードが読みにくい
  2. エラー処理の分散: try-catchが使えず、エラー処理がバラバラになる
  3. フロー制御の難しさ: 並列処理や直列処理の管理が複雑
  4. 信頼性の問題: コールバックが複数回呼ばれたり、全く呼ばれない可能性

Promiseのメリット

  1. チェーン可能: .then()でつなげて読みやすいコードになる
  2. 統一的エラー処理: チェーンの最後に.catch()で一括処理可能
  3. フロー制御の容易さ: Promise.all()など便利なメソッドが用意されている
  4. 信頼性: 状態が不変で、一度解決すると変化しない

Promiseの基本的な使い方

Promiseの作成

const promise = new Promise((resolve, reject) => {
  // 非同期処理を実行
  // 成功時: resolve(値)を呼び出す
  // 失敗時: reject(エラー)を呼び出す
});

Promiseの消費

promise
  .then((result) => {
    // 成功時の処理
  })
  .catch((error) => {
    // 失敗時の処理
  })
  .finally(() => {
    // 成功・失敗に関わらず実行される処理
  });

Promiseチェーン

Promiseの真価は、複数の非同期処理を連結できる点にあります。

function asyncTask1() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("結果1"), 1000);
  });
}

function asyncTask2(data) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(data + " → 結果2"), 1000);
  });
}

function asyncTask3(data) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(data + " → 結果3"), 1000);
  });
}

asyncTask1()
  .then(asyncTask2)
  .then(asyncTask3)
  .then((finalResult) => {
    console.log(finalResult); // "結果1 → 結果2 → 結果3"
  })
  .catch((error) => {
    console.error("エラー発生:", error);
  });

このように、.then()でつなげていくことで、非同期処理を同期処理のように直列的に記述できます。

エラー処理

Promiseでは.catch()を使ってエラーを捕捉します。チェーンのどこでエラーが発生しても、最後の.catch()で処理できます。

function mightFail() {
  return new Promise((resolve, reject) => {
    const random = Math.random();
    if (random > 0.5) {
      resolve("成功!");
    } else {
      reject("失敗...");
    }
  });
}

mightFail()
  .then((result) => {
    console.log(result);
    return mightFail(); // もう一度実行
  })
  .then((result) => {
    console.log("2回目も成功:", result);
  })
  .catch((error) => {
    console.error("エラー:", error); // どちらかのmightFailが失敗するとここで捕捉
  });

Promiseの静的メソッド

Promiseには便利な静的メソッドが用意されています。

Promise.all()

複数のPromiseを並列実行し、すべてが成功したら処理を続行します。

const promise1 = fetch("/api/data1");
const promise2 = fetch("/api/data2");
const promise3 = fetch("/api/data3");

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    // resultsは[data1, data2, data3]の配列
    console.log("すべてのデータ取得完了:", results);
  })
  .catch((error) => {
    // 1つでも失敗するとここに入る
    console.error("いずれかの取得に失敗:", error);
  });

Promise.race()

複数のPromiseのうち、最初に完了したもの(成功・失敗問わず)の結果を使います。

const timeout = new Promise((_, reject) => {
  setTimeout(() => reject("タイムアウト"), 5000);
});
const fetchPromise = fetch("/api/data");

Promise.race([fetchPromise, timeout])
  .then((data) => {
    console.log("データ取得成功:", data);
  })
  .catch((error) => {
    console.error("エラー:", error); // 取得が5秒以上かかるとタイムアウト
  });

Promise.allSettled()

すべてのPromiseが完了(成功または失敗)するのを待ちます。

const promises = [
  fetch("/api/data1"),
  fetch("/api/data2").catch(() => "デフォルト値"),
  Promise.reject("明示的なエラー")
];

Promise.allSettled(promises)
  .then((results) => {
    results.forEach((result) => {
      if (result.status === "fulfilled") {
        console.log("成功:", result.value);
      } else {
        console.log("失敗:", result.reason);
      }
    });
  });

Promiseの実践的使用例

例1: fetch APIを使ったHTTPリクエスト

function fetchUserData(userId) {
  return fetch(`https://api.example.com/users/${userId}`)
    .then((response) => {
      if (!response.ok) {
        throw new Error("ネットワークレスポンスが正常ではありません");
      }
      return response.json();
    })
    .then((user) => {
      console.log("ユーザーデータ:", user);
      return user;
    });
}

fetchUserData(123)
  .catch((error) => {
    console.error("データ取得に失敗:", error);
  });

例2: タイムアウト処理

function withTimeout(promise, timeoutMs) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`操作がタイムアウトしました(${timeoutMs}ms)`));
    }, timeoutMs);
  });

  return Promise.race([promise, timeout]);
}

// 使用例
const slowFetch = fetch("/api/data").then((r) => r.json());

withTimeout(slowFetch, 3000)
  .then((data) => console.log("データ:", data))
  .catch((error) => console.error("エラー:", error));

Promiseのよくある間違い

間違い1: Promiseコンストラクタの誤用

// 悪い例
function getData() {
  return new Promise((resolve) => {
    fetch("/api/data").then(resolve);
  });
}

// 良い例
function getData() {
  return fetch("/api/data");
}

間違い2: エラー処理の忘れ

// 悪い例
fetch("/api/data")
  .then((response) => response.json());

// 良い例
fetch("/api/data")
  .then((response) => response.json())
  .catch((error) => console.error("エラー:", error));

間違い3: Promiseチェーンの分断

// 悪い例
const promise = fetch("/api/data")
  .then((response) => response.json());

promise.then((data) => console.log(data));
promise.then((data) => processData(data)); // 別のチェーンになる

// 良い例
fetch("/api/data")
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
    return processData(data);
  });

Promiseのベストプラクティス

  1. 常にエラー処理を行う: .catch()try/catch(async/await時)を必ず使う
  2. Promiseを適切に返す: チェーンを分断しない
  3. 不必要なネストを避ける: .then()はフラットに保つ
  4. 名前付き関数を使う: 複雑な処理は関数として定義
  5. Promise化を活用: コールバックスタイルのAPIをPromiseでラップ

Promiseとasync/awaitの関係

次章で詳しく学ぶasync/awaitはPromiseのシンタックスシュガー(構文糖)です。async関数は常にPromiseを返し、awaitはPromiseの解決を待ちます。

async function fetchData() {
  try {
    const response = await fetch("/api/data");
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error("エラー:", error);
    throw error;
  }
}

// async関数はPromiseを返すので、then/catchも使える
fetchData().then((data) => processData(data));

まとめ

Promiseについての重要なポイント:

  • Promiseは非同期処理の最終的な完了(成功/失敗)とその結果を表現するオブジェクト
  • pending(待機)、fulfilled(履行)、rejected(拒否)の3つの状態がある
  • .then()で成功時の処理、.catch()でエラー処理を記述
  • チェーンすることで複数の非同期処理を順番に実行できる
  • Promise.all()Promise.race()などの便利な静的メソッドがある
  • コールバック地獄の問題を解決できる
  • async/awaitの基礎となる概念
  • 現代JavaScriptの非同期処理の基盤

Promiseをマスターすることで、非同期コードをより宣言的で管理しやすい形で記述できるようになります。

練習問題

問題1

以下のコードの実行順序と出力結果を予想してください。

console.log("スクリプト開始");

const promise = new Promise((resolve) => {
  console.log("Promise実行");
  setTimeout(() => {
    resolve("Promise解決");
    console.log("タイマーコールバック");
  }, 0);
});

promise.then((msg) => {
  console.log(msg);
});

console.log("スクリプト終了");

問題2

次のPromiseの特徴について、正しいものには○、間違っているものには×をつけてください。

  1. Promiseの状態は一度解決すると変化しない ( )
  2. .then()メソッドは新しいPromiseを返す ( )
  3. Promise.all()は渡されたPromiseのうち1つでも失敗すると即時 reject する ( )
  4. .finally()コールバックは解決値や拒否理由を受け取ることができる ( )

問題3

以下のコールバックスタイルの関数をPromiseでラップするコードを書いてください。

function readFile(path, encoding, callback) {
  // ファイルを読み込む非同期処理
  // 成功時: callback(null, data)
  // 失敗時: callback(error)
}

解答例

問題1の解答

スクリプト開始
Promise実行
スクリプト終了
タイマーコールバック
Promise解決

説明:

  1. 同期的なコードが最初に実行される
  2. Promiseコンストラクタ内の同期的なコード(console.log)が実行
  3. setTimeoutは非同期なので後回し
  4. スクリプトの同期的な実行が終了
  5. マイクロタスク(Promise)よりも先にマクロタスク(setTimeout)が実行
  6. Promiseが解決され、thenのコールバックが実行

問題2の解答

  1. × (finallyコールバックは引数を受け取らない)

問題3の解答

function readFilePromise(path, encoding) {
  return new Promise((resolve, reject) => {
    readFile(path, encoding, (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

// 使用例
readFilePromise("example.txt", "utf8")
  .then((data) => console.log(data))
  .catch((error) => console.error("読み込みエラー:", error));