【JavaScript】改めてPromiseを理解する
概要
JavaScript
のPromise
について「何も分かってない。雰囲気で使ってる😇」と常々感じていたので、いい加減自分なりにちゃんと理解しようと思って書いた記事です。
Promiseとは
Promise
は、非同期処理の状態や結果を表す組み込みオブジェクトです。
非同期関数は、実行してもすぐに結果が分からないため、その状態や結果を扱うための機能が必要です。それがPromise
オブジェクトであり、非同期処理の状態や結果をラップしたようなオブジェクトというイメージです。
Promiseの状態
上で「非同期処理の状態」と書きました。Promiseではこの状態を表すものとして以下の3つが存在します。
-
Fulfilled
: 成功resolve
した時の状態 -
Rejected
: 失敗reject
や例外が発生した時の状態 -
Pending
:Fulfilled
でもRejected
でもない状態。Promiseインスタンス生成時の初期状態はこれ
この状態は、内部的な状態なので、Promise
インスタンスから直接この状態を参照したり、更新することはできません。この状態は、後述するresolve
やreject
などのメソッドを通じて変更されるものです。
なお、Promiseインスタンスの状態はPending
で始まり、その後一度でもFulfilled
かRejected
に変更されると、それ以降は変化しなくなります。
Promiseを使った処理
基本的な使い方
Promise
はnew演算子でインスタンスを作成し、resolve
とreject
の2つの引数を持つ関数を渡します。resolve
はこの非同期処理が成功した時に呼ぶ関数で、reject
は失敗した時に呼ぶ関数です。
const promise = new Promise((resolve, reject) => {
// 非同期処理
// 成功した場合は,resolve(結果)を呼び出す
// 失敗した場合は,reject(エラーオブジェクト)を呼び出す
});
簡単なサンプルでPromiseの挙動を理解する
簡単な例で、Promise
の挙動を確認していきます。
const checkNumber = (input) => {
return new Promise((resolve, reject) => {
console.log("Promise開始");
// 数値以外の入力に対して例外をスロー
if (typeof input !== "number") {
throw new Error("入力は数値でなければなりません");
}
// 数値が5以上かどうかを判定
if (input >= 5) {
resolve(`成功: ${input} は5以上です`);
} else {
reject(`失敗: ${input} は5未満です`);
}
});
};
// Promiseの実行
checkNumber(6)
// 成功のケース(=resolveが呼ばれた時)
.then((message) => {
console.log("Promise fulfilled:", message);
})
// 失敗のケース(=rejectが呼ばれた時)
.catch((error) => {
console.log("Promise rejected:", error);
});
resolve
とreject
はPromise
の状態を変更するために呼ばれる関数です。Promise
オブジェクト内で使用されます。それぞれのメソッドについて見ていきましょう。
resolve → then
resolve
はPromise
が成功した時に呼ぶ関数です。実行後、Promise
の状態はFulfilled
となります。多くの場合、処理結果の値をresolve
に渡します。サンプルでは、成功: ${num} は5以上です
という成功を示すメッセージが渡されています。
Promise
オブジェクト内のresolve
が実行されると、Promise
オブジェクトにチェーンされたthen
メソッドが実行されます。この時、resolve
に渡した値がthen
に渡されます。
// ...略
resolve(`成功: ${num} は5以上です`);
// ...略
.then((message) => {
console.log("Promise fulfilled:", message);
})
今回の例では、resolve
に渡された成功: ${num} は5以上です
というメッセージがthen
メソッドに渡され、最終的にPromise fulfilled: 成功: 6 は5以上です
というメッセージがログ出力されます。
reject → catch
reject
はPromise
が失敗した時に呼ぶ関数です。実行後、Promise
の状態はRejected
となります。多くの場合、エラーメッセージやオブジェクトをreject
に渡します。上のサンプルでは、失敗: ${num} は5未満です
というエラーメッセージが渡されています。
Promise
オブジェクト内のreject
が実行されると、Promise
オブジェクトにチェーンされたcatch
メソッドが実行されます。この時、reject
に渡した値がcatch
に渡されます。
// ...略
reject(`失敗: ${num} は5未満です`);
// ...略
checkNumber(3)
// 成功のケース(=resolveが呼ばれた時)
.then((message) => {
console.log("Promise fulfilled:", message);
})
// 失敗のケース(=rejectが呼ばれた時)
.catch((error) => {
console.log("Promise rejected:", error);
});
今回の例では、reject
に渡された失敗: ${num} は5未満です
というメッセージがcatch
メソッドに渡され、最終的にPromise rejected: 失敗: 3 は5未満です
というメッセージがログ出力されます。
Promiseチェーンにおける例外処理
なお、catch
はreject
が呼ばれた時に実行されるだけでなく、処理中に発生した例外も捕捉します。Promise
チェーンのどこかで例外が発生すると、catch
までスキップされます。
実際にサンプルでも確認してみます。本サンプルでは与えられた値が数値以外の場合に例外を発生させるようになっているため、文字列を渡して実行します。
checkNumber("test")
.then((message) => {
console.log("Promise fulfilled:", message);
})
.catch((error) => {
console.log("Promise rejected or exception", error);
});
すると、例外が発生し、catch
内の処理が実行されることが確認できます。
Promiseを使ったfetch APIの非同期処理
Promise
の基本的な使い方を理解したところで、次はfetch
を用いたもう少し実用的な非同期処理の例を見ていきます。
以下は、fetch
を使った典型的なAPI呼び出しの非同期処理です。
fetch
は、ブラウザやNode.js
環境で非同期HTTPリクエストを送信するための機能です。このfetch
も内部的にPromise
を返しており、その中で非同期処理の結果に応じてresolve
やreject
が呼び出されています。
そのため、上で見たサンプル関数checkNumber
同様に、then
メソッドでチェーンして、非同期処理の結果データを受け取り、後続の処理を記述できます。
const fetchPromise = fetch(`https://jsonplaceholder.typicode.com/users/134343`)
fetchPromise
.then((response) => {
if (!response.ok) {
// ステータスコードがエラーの場合はエラーをスロー
throw new Error(`HTTP Error: ${response.status}`);
}
// Promise<Response>を返す
return response.json();
})
// response.jsonがPromiseを返すのでthenでチェーンして、後続処理を行う
.then((user) => console.log("ユーザー情報を取得しました:",user))
.catch((error) => {
// errorオブジェクトはPromiseではない
console.error("エラーが発生しました:", error);
});
では、試しにHTTPステータスが成功(2XX)以外の時に例外を発生させる部分を削除してみます。この場合、HTTPレスポンスステータス自体はエラーだとしても、Promise
としては成功で返却されます。そのため、catch
ではなく、then
へと続いていきます。
// ユーザー情報を取得する非同期関数
const fetchPromise = fetch(`https://jsonplaceholder.typicode.com/users/134343`)
fetchPromise
.then((response) => {
- if (!response.ok) {
- // ステータスコードがエラーの場合はエラーをスロー
- throw new Error(`HTTP Error: ${response.status}`);
- }
// Promise<Response>を返す
return response.json();
})
// response.jsonがPromiseを返すのでthenでチェーンして、後続処理を行う
.then((user) => console.log("ユーザー情報を取得しました:",user))
.catch((error) => {
// errorオブジェクトはPromiseではない
console.error("エラーが発生しました:", error);
});
Responseオブジェクト
Response
オブジェクトは、fetchAPI
によって返されるレスポンスを便利な機能でラップしたオブジェクトです。このオブジェクトには、レスポンスの状態やデータを扱うための便利なメソッドやプロパティが含まれています。
主なプロパティやメソッドとしては、以下のようなものがあります。
Response.ok
レスポンスが成功(HTTPステータスコードが200~299の範囲)であるかを示すプロパティです。
if (!response.ok) {
// ステータスコードがエラーの場合はエラーをスロー
throw new Error(`HTTP Error: ${response.status}`);
}
Response.json()
レスポンスボディに含まれるJSON形式の文字列をパース(解析)して、JavaScriptのオブジェクトに変換するのがresponse.json
です。response.json
変換されたJavaScript
オブジェクトを含むPromise
オブジェクトを返します。
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json()) // JSON文字列をJavaScriptオブジェクトに変換
.then(data => {
console.log(data); // JavaScriptオブジェクトとして利用可能になる
});
ネットワークを介して送信されてくるデータは、通常は文字列です。例えば、サーバーから送られてくるJSONデータは単なる文字列であり、JavaScript
オブジェクトではありません。そのため、受信したデータ(JSON文字列)をJavaScript
で扱う前にJavaScript
オブジェクトに変換する必要があるのです。その役割を担うのがresponse.json
です。
response.json
はPromise
オブジェクトを返すので、変換した結果はthen
で繋いで利用します。
参考
最後に
以上です。
then
やresponse.json
などは習慣のように使ってましたが、改めてちゃんと理解すると色々と発見がありました。
Discussion