はじめに

JavaScriptで非同期処理を扱うとき、避けて通れないのが「Promise(プロミス)」です。APIからデータを取得する、ファイルを読み込む、一定時間後に処理を実行するなど、多くの場面でPromiseは活躍します。

かつてはコールバック関数を使った非同期処理が主流でしたが、処理が複雑になると「コールバック地獄」と呼ばれる読みづらいコードに陥りがちでした。Promiseはこの問題を解決し、非同期処理を直感的に書けるようにする仕組みです。

本記事では、以下の内容を初心者向けにわかりやすく解説します。

  • Promiseとは何か
  • Promiseの3つの状態
  • resolverejectの使い方
  • thencatchfinallyによるチェーン処理
  • Promiseを使う際の注意点
  • 実践的な活用例

Promiseとは

Promiseの基本概念

Promiseとは、非同期処理の結果(成功または失敗)を表すオブジェクトです。「将来のある時点で値を返すことを約束する」というイメージから、Promise(約束)という名前が付けられています。

1
2
3
4
5
6
// Promiseの基本的な構造
const promise = new Promise((resolve, reject) => {
  // 非同期処理を実行
  // 成功した場合: resolve(値)
  // 失敗した場合: reject(エラー)
});

Promiseを使うと、非同期処理の完了を待ってから次の処理を実行したり、エラーが発生した場合に適切に対処したりできます。

なぜPromiseが必要なのか

JavaScriptはシングルスレッドで動作するため、時間のかかる処理を同期的に実行すると、その間他の処理がブロックされてしまいます。非同期処理を使えば、時間のかかる処理を待っている間も他の処理を続けられます。

従来のコールバック方式では、複数の非同期処理を連続して実行すると、ネストが深くなり可読性が低下していました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// コールバック地獄の例(アンチパターン)
getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      getYetMoreData(c, function(d) {
        console.log(d);
      });
    });
  });
});

Promiseを使えば、この問題を解決できます。

1
2
3
4
5
6
// Promiseを使った書き方
getData()
  .then((a) => getMoreData(a))
  .then((b) => getEvenMoreData(b))
  .then((c) => getYetMoreData(c))
  .then((d) => console.log(d));

Promiseの3つの状態

Promiseは、作成されてから完了するまでに3つの状態を持ちます。

状態 英語名 説明
待機中 pending 初期状態。まだ成功も失敗もしていない
履行済み fulfilled 処理が成功して完了した
拒否済み rejected 処理が失敗した
stateDiagram-v2
    [*] --> pending: Promise生成
    pending --> fulfilled: resolve()を呼び出し
    pending --> rejected: reject()を呼び出し
    fulfilled --> [*]
    rejected --> [*]

一度「履行済み」または「拒否済み」になったPromiseは、その後状態が変わることはありません。この「履行済み」と「拒否済み」を合わせて「決定済み(settled)」と呼びます。

1
2
3
4
const promise = new Promise((resolve, reject) => {
  resolve("成功"); // 履行済み(fulfilled)になる
  reject("失敗"); // この行は無視される(すでに決定済みのため)
});

Promiseの基本構文

Promiseの作成

Promiseはnew Promise()コンストラクターを使って作成します。コンストラクターには「実行関数(executor)」と呼ばれる関数を渡します。

1
2
3
const myPromise = new Promise((resolve, reject) => {
  // 非同期処理をここに書く
});

実行関数は2つの引数を受け取ります。

引数 説明
resolve 処理が成功したときに呼び出す関数。引数に成功時の値を渡す
reject 処理が失敗したときに呼び出す関数。引数にエラーを渡す

resolveとrejectの使い方

resolveは処理の成功を、rejectは処理の失敗を表します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 成功する例
const successPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("処理が完了しました");
  }, 1000);
});

// 失敗する例
const failPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("処理に失敗しました"));
  }, 1000);
});

rejectにはErrorオブジェクトを渡すことが推奨されます。エラーオブジェクトにはスタックトレースが含まれるため、デバッグがしやすくなります。

実践的な例:ランダムに成功・失敗するPromise

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function randomOperation() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const random = Math.random();
      if (random > 0.5) {
        resolve(`成功!乱数: ${random}`);
      } else {
        reject(new Error(`失敗!乱数: ${random}`));
      }
    }, 1000);
  });
}

then・catch・finallyの使い方

then()メソッド

then()メソッドは、Promiseが履行されたときに実行するコールバック関数を登録します。

1
2
3
4
5
6
7
8
9
const promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve("Hello, Promise!");
  }, 1000);
});

promise.then((value) => {
  console.log(value); // "Hello, Promise!"
});

then()は2つの引数を受け取ることもできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
promise.then(
  (value) => {
    // 成功時の処理
    console.log("成功:", value);
  },
  (error) => {
    // 失敗時の処理
    console.log("失敗:", error);
  }
);

ただし、エラー処理には後述するcatch()を使う方が一般的です。

catch()メソッド

catch()メソッドは、Promiseが拒否されたときに実行するコールバック関数を登録します。

1
2
3
4
5
6
7
8
9
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("エラーが発生しました"));
  }, 1000);
});

promise.catch((error) => {
  console.error("エラーをキャッチ:", error.message);
});

catch()then(null, onRejected)の省略形です。チェーンの最後にcatch()を配置することで、チェーン内のどこでエラーが発生しても捕捉できます。

finally()メソッド

finally()メソッドは、Promiseの結果(成功・失敗)に関わらず、最後に必ず実行する処理を登録します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("完了");
  }, 1000);
});

promise
  .then((value) => {
    console.log("成功:", value);
  })
  .catch((error) => {
    console.error("失敗:", error);
  })
  .finally(() => {
    console.log("処理終了"); // 成功でも失敗でも実行される
  });

finally()は、ローディング表示の終了やリソースのクリーンアップなど、結果に関係なく実行したい処理に適しています。

3つのメソッドを組み合わせた例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    console.log("データを取得中...");
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: "太郎", age: 25 });
      } else {
        reject(new Error("無効なユーザーIDです"));
      }
    }, 1500);
  });
}

fetchUserData(1)
  .then((user) => {
    console.log("ユーザー情報:", user);
  })
  .catch((error) => {
    console.error("エラー:", error.message);
  })
  .finally(() => {
    console.log("データ取得処理が完了しました");
  });
// 出力:
// データを取得中...
// ユーザー情報: { id: 1, name: '太郎', age: 25 }
// データ取得処理が完了しました

Promiseチェーン

チェーンの基本

then()メソッドは新しいPromiseを返すため、複数のthen()を連結(チェーン)できます。これを「Promiseチェーン」と呼びます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
new Promise((resolve) => {
  resolve(1);
})
  .then((value) => {
    console.log(value); // 1
    return value + 1;
  })
  .then((value) => {
    console.log(value); // 2
    return value + 1;
  })
  .then((value) => {
    console.log(value); // 3
  });

then()のコールバックでreturnした値が、次のthen()の引数として渡されます。

Promiseを返すチェーン

then()のコールバックでPromiseを返すと、そのPromiseが解決されるまで次のthen()は待機します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

wait(1000)
  .then(() => {
    console.log("1秒経過");
    return wait(1000);
  })
  .then(() => {
    console.log("2秒経過");
    return wait(1000);
  })
  .then(() => {
    console.log("3秒経過");
  });

実践的なチェーンの例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function getUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: userId, name: "太郎" });
    }, 500);
  });
}

function getOrders(user) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, product: "商品A", userId: user.id },
        { id: 2, product: "商品B", userId: user.id },
      ]);
    }, 500);
  });
}

function getOrderDetails(orders) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const details = orders.map((order) => ({
        ...order,
        price: Math.floor(Math.random() * 10000),
      }));
      resolve(details);
    }, 500);
  });
}

getUser(1)
  .then((user) => {
    console.log("ユーザー取得:", user.name);
    return getOrders(user);
  })
  .then((orders) => {
    console.log("注文数:", orders.length);
    return getOrderDetails(orders);
  })
  .then((details) => {
    console.log("注文詳細:", details);
  })
  .catch((error) => {
    console.error("エラー:", error.message);
  });

Promiseを使う際の注意点

注意点1:returnを忘れない

then()のコールバック内でPromiseを返すときは、必ずreturnを付けましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 悪い例:returnを忘れている
doSomething()
  .then((result) => {
    fetchData(result); // returnがないため、次のthenはundefinedを受け取る
  })
  .then((data) => {
    console.log(data); // undefined
  });

// 良い例:returnを付けている
doSomething()
  .then((result) => {
    return fetchData(result);
  })
  .then((data) => {
    console.log(data); // fetchDataの結果
  });

注意点2:エラーハンドリングを忘れない

Promiseチェーンには必ずcatch()を付けて、エラーを適切に処理しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 悪い例:catchがない
doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult));
// エラーが発生しても、どこにも通知されない(未処理の拒否)

// 良い例:catchを付けている
doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .catch((error) => {
    console.error("エラーが発生しました:", error);
  });

注意点3:Promiseコンストラクター内での例外

new Promise()の実行関数内で例外が発生すると、自動的にPromiseが拒否されます。

1
2
3
4
5
6
7
const promise = new Promise((resolve, reject) => {
  throw new Error("例外が発生!");
});

promise.catch((error) => {
  console.log("キャッチ:", error.message); // "例外が発生!"
});

注意点4:thenのコールバック内での例外

then()のコールバック内で例外が発生した場合も、後続のcatch()で捕捉できます。

1
2
3
4
5
6
7
Promise.resolve("OK")
  .then((value) => {
    throw new Error("thenの中で例外!");
  })
  .catch((error) => {
    console.log("キャッチ:", error.message); // "thenの中で例外!"
  });

注意点5:Promise.resolve()とPromise.reject()

すでに決定済みのPromiseを作成するには、静的メソッドを使います。

1
2
3
4
5
6
7
8
// 履行済みのPromiseを作成
const fulfilled = Promise.resolve("成功した値");

// 拒否済みのPromiseを作成
const rejected = Promise.reject(new Error("失敗の理由"));

fulfilled.then((value) => console.log(value)); // "成功した値"
rejected.catch((error) => console.log(error.message)); // "失敗の理由"

複数のPromiseを扱う

複数の非同期処理を同時に実行したい場合、Promiseの静的メソッドを使います。

Promise.all()

すべてのPromiseが成功したときに履行され、1つでも失敗すると即座に拒否されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // [1, 2, 3]
  })
  .catch((error) => {
    console.error("いずれかが失敗:", error);
  });

Promise.allSettled()

すべてのPromiseが決定(成功または失敗)するまで待ちます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const promise1 = Promise.resolve("成功");
const promise2 = Promise.reject(new Error("失敗"));
const promise3 = Promise.resolve("成功2");

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    results.forEach((result) => {
      if (result.status === "fulfilled") {
        console.log("成功:", result.value);
      } else {
        console.log("失敗:", result.reason.message);
      }
    });
  });
// 出力:
// 成功: 成功
// 失敗: 失敗
// 成功: 成功2

Promise.race()

最初に決定したPromiseの結果を返します。

1
2
3
4
5
6
7
const slow = new Promise((resolve) => setTimeout(() => resolve("遅い"), 2000));
const fast = new Promise((resolve) => setTimeout(() => resolve("速い"), 500));

Promise.race([slow, fast])
  .then((value) => {
    console.log(value); // "速い"
  });

Promise.any()

最初に成功したPromiseの結果を返します。すべて失敗した場合のみ拒否されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const fail1 = Promise.reject(new Error("失敗1"));
const fail2 = Promise.reject(new Error("失敗2"));
const success = Promise.resolve("成功");

Promise.any([fail1, fail2, success])
  .then((value) => {
    console.log(value); // "成功"
  })
  .catch((error) => {
    console.log("すべて失敗:", error);
  });
メソッド 履行条件 拒否条件
Promise.all() すべて成功 1つでも失敗
Promise.allSettled() すべて決定後、常に履行 なし
Promise.race() 最初に決定したものに従う 最初に決定したものに従う
Promise.any() 最初に成功 すべて失敗

実践例:APIデータの取得

Promiseを使った実践的な例として、fetch APIでデータを取得する処理を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function fetchTodo(id) {
  return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`HTTPエラー: ${response.status}`);
      }
      return response.json();
    });
}

fetchTodo(1)
  .then((todo) => {
    console.log("取得したTodo:", todo);
    console.log("タイトル:", todo.title);
  })
  .catch((error) => {
    console.error("取得に失敗:", error.message);
  })
  .finally(() => {
    console.log("API呼び出し完了");
  });

fetch()自体がPromiseを返すため、そのままthen()でチェーンできます。response.json()もPromiseを返すので、ここでもreturnが必要です。

async/awaitとの関係

ES2017で導入されたasync/await構文を使うと、Promiseをより直感的に扱えます。async/awaitはPromiseの上に構築された構文糖衣です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Promiseチェーンでの書き方
function fetchDataWithPromise() {
  return getUser(1)
    .then((user) => getOrders(user))
    .then((orders) => getOrderDetails(orders))
    .then((details) => {
      console.log(details);
      return details;
    })
    .catch((error) => {
      console.error(error);
    });
}

// async/awaitでの書き方
async function fetchDataWithAsync() {
  try {
    const user = await getUser(1);
    const orders = await getOrders(user);
    const details = await getOrderDetails(orders);
    console.log(details);
    return details;
  } catch (error) {
    console.error(error);
  }
}

async/awaitを使うと、同期的なコードのように書けるため、可読性が向上します。ただし、Promiseの仕組みを理解していることが前提となります。

まとめ

本記事では、JavaScriptのPromiseについて基本から実践的な使い方まで解説しました。

  • Promiseは非同期処理の結果を表すオブジェクトで、「待機中」「履行済み」「拒否済み」の3つの状態を持つ
  • resolveで成功、rejectで失敗を表す
  • then()で成功時、catch()で失敗時、finally()で常に実行する処理を登録する
  • then()は新しいPromiseを返すため、チェーンで連結できる
  • 複数のPromiseを扱うにはPromise.all()Promise.allSettled()Promise.race()Promise.any()を使う
  • async/awaitはPromiseをより直感的に書くための構文

Promiseを理解することで、API通信やタイマー処理など、非同期処理を伴うアプリケーション開発がスムーズになります。async/await構文もPromiseがベースになっているため、まずはPromiseの基本をしっかり押さえておきましょう。

参考リンク