🗼

【JavaScript】改めてPromiseを理解する

2025/01/18に公開

概要

JavaScriptPromiseについて「何も分かってない。雰囲気で使ってる😇」と常々感じていたので、いい加減自分なりにちゃんと理解しようと思って書いた記事です。

Promiseとは

Promiseは、非同期処理の状態や結果を表す組み込みオブジェクトです。

非同期関数は、実行してもすぐに結果が分からないため、その状態や結果を扱うための機能が必要です。それがPromiseオブジェクトであり、非同期処理の状態や結果をラップしたようなオブジェクトというイメージです。

Promiseの状態

上で「非同期処理の状態」と書きました。Promiseではこの状態を表すものとして以下の3つが存在します。

  • Fulfilled: 成功resolveした時の状態
  • Rejected: 失敗rejectや例外が発生した時の状態
  • Pending: FulfilledでもRejectedでもない状態。Promiseインスタンス生成時の初期状態はこれ

この状態は、内部的な状態なので、Promiseインスタンスから直接この状態を参照したり、更新することはできません。この状態は、後述するresolverejectなどのメソッドを通じて変更されるものです。

なお、Promiseインスタンスの状態はPendingで始まり、その後一度でもFulfilledRejectedに変更されると、それ以降は変化しなくなります。

https://jsprimer.net/basic/async/#promise-status

Promiseを使った処理

基本的な使い方

Promiseはnew演算子でインスタンスを作成し、resolverejectの2つの引数を持つ関数を渡します。resolveはこの非同期処理が成功した時に呼ぶ関数で、rejectは失敗した時に呼ぶ関数です。

const promise = new Promise((resolve, reject) => {
    // 非同期処理
    // 成功した場合は,resolve(結果)を呼び出す
    // 失敗した場合は,reject(エラーオブジェクト)を呼び出す
});

https://jsprimer.net/basic/async/#promise-instance

簡単なサンプルで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);
    });

resolverejectPromiseの状態を変更するために呼ばれる関数です。Promiseオブジェクト内で使用されます。それぞれのメソッドについて見ていきましょう。

resolve → then

resolvePromiseが成功した時に呼ぶ関数です。実行後、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

rejectPromiseが失敗した時に呼ぶ関数です。実行後、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チェーンにおける例外処理

なお、catchrejectが呼ばれた時に実行されるだけでなく、処理中に発生した例外も捕捉します。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呼び出しの非同期処理です。

https://developer.mozilla.org/ja/docs/Web/API/Fetch_API
https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch

fetchは、ブラウザやNode.js環境で非同期HTTPリクエストを送信するための機能です。このfetchも内部的にPromiseを返しており、その中で非同期処理の結果に応じてresolverejectが呼び出されています。

そのため、上で見たサンプル関数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によって返されるレスポンスを便利な機能でラップしたオブジェクトです。このオブジェクトには、レスポンスの状態やデータを扱うための便利なメソッドやプロパティが含まれています。

https://developer.mozilla.org/ja/docs/Web/API/Response

主なプロパティやメソッドとしては、以下のようなものがあります。

Response.ok

レスポンスが成功(HTTPステータスコードが200~299の範囲)であるかを示すプロパティです。

if (!response.ok) {
    // ステータスコードがエラーの場合はエラーをスロー
    throw new Error(`HTTP Error: ${response.status}`);
}

https://developer.mozilla.org/ja/docs/Web/API/Response/ok

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.jsonPromiseオブジェクトを返すので、変換した結果はthenで繋いで利用します。

https://developer.mozilla.org/ja/docs/Web/API/Response/json

参考

https://jsprimer.net/basic/async/#promise
https://jsprimer.net/use-case/ajaxapp/promise/

最後に

以上です。

thenresponse.jsonなどは習慣のように使ってましたが、改めてちゃんと理解すると色々と発見がありました。

Discussion