Promiseの状態遷移をちゃんとまとめてみた

# はじめに
`Promise`の解説サイトを見ると「resolve関数を呼ぶとfulfilledになります」といった表現が使われるなど、ちゃんとした状態遷移の説明がないことがあったので、自分の理解用に状態遷移図を作成してみました。基本的に勉強したのは「`Promise`コンストラクター」のみです（[参考URL](#参考url)）。独学のため間違っているところがあるかもしれません。

# 状態遷移図
`Promise`には排他的な3つの状態があります。  
   * **<font color="#548235">pending</font>** : 初期および待機状態。**<font color="#2E75B6">fulfilled</font>** でも **<font color="#C55A11">rejected</font>** でもない状態。
   * **<font color="#2E75B6">fulfilled</font>** : 処理が完全に成功した状態。
   * **<font color="#C55A11">rejected</font>** : 処理が失敗した状態。  
![states.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3193449/cea19fed-3c7c-42cf-7e22-d8b2b38f10c9.png)

:::note warn
* 全体として、正確さよりも理解のしやすさを優先して表現しています（実際に理解しやすくなっているかは別ですが）。また、状態確定など、この記事内でしか出てこない単語もあるため[参考URL](#参考url)と見比べながら理解しようとすると混乱する可能性があります。
* **unresolved**、**resolved**、**locked in**、**settled**については説明用であり、「`Promise`が**setteled**になる」というような言い方はしても、`Promise`の状態が実際に**setteled**になる（**pending**、**fulfilled**、**rejected**以外の状態になる）ことはありません。 
* **unresolved**、**resolved**については`States`ではなく`Fates`と表現されており、また**fulfilled**や**settled**が`it is fulfilled`、`it is settled`などのように記載されているのに対し、**locked in**については`it has been "locked in"`と記載されています。そのため、これら（3つの状態以外）を同じように扱うのは好ましくないかもしれませんが、概要把握を優先し状態として扱っています。
:::

# イメージの説明
例えば`Promise`の処理が「ファイルの中身を読み取ってその内容を返す処理」の場合  

* 処理の全てを自分で行う場合
  * 処理がすべて正常に動作した場合 → 処理は完全に成功（「ファイルの中身を読み取ってその内容を返す処理」を行うという約束は履行された）  
   **<font color="#548235">unresolved</font>** → (b) → **<font color="#2E75B6">fulfilled</font>**  
  * 処理が途中で失敗した場合 → 処理失敗（「ファイルの中身を読み取ってその内容を返す処理」を行うという約束は拒否された）  
   **<font color="#548235">unresolved</font>** → **<font color="#C55A11">rejected</font>**  

* ファイルのパスの取得だけを自分で処理し、パスからファイルの中身を読み取って内容を返す処理は別の`Promise`の処理とした（別の`Promise`に任せた）場合  
  * パスの取得が成功し、別の処理も成功した場合 → 処理は完全に成功  
  **<font color="#548235">unresolved</font>** →(a)→ **<font color="#548235">locked in</font>**(別の処理実行中) → 別の処理成功で **<font color="#2E75B6">fulfilled</font>**  
  * パスの取得は成功したが、別の処理が失敗した場合 → 処理失敗  
  **<font color="#548235">unresolved</font>** →(a)→ **<font color="#548235">locked in</font>**(別の処理実行中) → 別の処理失敗で **<font color="#C55A11">rejected</font>**  
  * パスの取得が失敗した場合 → 処理失敗  
   **<font color="#548235">unresolved</font>** → **<font color="#C55A11">rejected</font>**  

# サンプルコード
resolve関数の引数がプリミティブの場合（数値や文字列など）や`thenable`でない場合は、直ぐに**fulfilled**になります。  
* `thenable`についての詳細は[こちら](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenable)を参照。つまり`Promise`オブジェクトと似たようなオブジェクトのことで`Promise`オブジェクトも`thenable`になります。よくわからない場合、まずは`thenable`を`Promise`オブジェクトに置き換えて読んでみてください。
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve(10);
});
console.log(promise1);
//Promise { <state>: "fulfilled", <value>: 10 }
```
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve(console);
});
console.log(promise1);
//Promise { <state>: "fulfilled", <value>: Console }
```
resolve関数の引数が`thenable`の場合は、その`thenable`の状態に依存します。`thenable`が**fulfilled**になると、自身も**fulfilled**になります。
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve(
        new Promise((resolveInner, rejectInnner) => {
            setTimeout(() => resolveInner(), 2000);
        })
    );
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//Promise { <state>: "fulfilled", <value>: undefined }
//Promise { <state>: "fulfilled", <value>: undefined }
//...
```
`thenable`が**rejected**になると、自身も**rejected**になります。
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve(
        new Promise((resolveInner, rejectInnner) => {
            setTimeout(() => rejectInnner(), 2000);
        })
    );
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//Promise { <state>: "rejected", <reason>: undefined }
//Uncaught (in promise) undefined
//Promise { <state>: "rejected", <reason>: undefined }
//...
```
なお、非同期のエラー処理については注意が必要です。以下は**fulfilled**になります。  
[JavaScriptと非同期のエラー処理 - Yahoo! JAPAN Tech Blog](https://techblog.yahoo.co.jp/programming/javascript_error/)
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve(
        new Promise((resolveInner, rejectInnner) => {
            setTimeout(() => { throw new Error("error") }, 500);
            setTimeout(() => resolveInner(), 2000);
        })
    );
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Uncaught Error: error
//Promise { <state>: "pending" }
//Promise { <state>: "fulfilled", <value>: undefined }
//Promise { <state>: "fulfilled", <value>: undefined }
//...
```
resolve関数の引数が`thenable`だとしても、それが自分自身の場合は**rejected**になります。
```javascript
const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve(promise1));//引数が自分自身
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Uncaught (in promise) TypeError: A promise cannot be resolved with itself.
//Promise { <state>: "rejected", <reason>: TypeError }
//Promise { <state>: "rejected", <reason>: TypeError }
//...
```
resolve関数の引数が`thenable`だとしても、`then`プロパティにアクセスすると例外を発生させるような場合は**rejected**になります。この場合、**resolved**になる前に（**unresolved**の状態で）例外が発生したので**rejected**になったわけではなく、また引数の`thenable`の状態が**rejected**になったのに伴い**rejected**になったわけでもないです[*](https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promise-resolve-functions)。
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve({
        get then() {
            throw new Error("error");
        },
    });
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: Error }
```
reject関数が呼ばれると、直ぐに**rejected**になります。
```javascript
const promise1 = new Promise((resolve, reject) => {
    reject();
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: undefined }
//Uncaught (in promise) undefined
```
例外が発生すると、直ぐに**rejected**になります。
```javascript
const promise1 = new Promise((resolve, reject) => {
    throw new Error('error');
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: Error }
//Uncaught (in promise) Error: error
```
処理するものがなくなっても成功（または失敗）にはなりません。以下はずっと**pending**のままです。
```javascript
const promise1 = new Promise((resolve, reject) => {
    //処理がないため即終了
});
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//...
```
一度処理が成功もしくは失敗した（**resolved**になった）後に、resolve関数やreject関数が呼ばれたり、例外が発生したりしても、状態に影響を与えることも結果の値が変わることもありません。影響するのは処理が成功も失敗もしていないとき（**unresolved**のとき）に呼ばれたり、発生したりしたときです。
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve(//呼ばれた時点でresolvedになり引数↓のPromiseにロックインされる。
        new Promise((resolveInner, rejectInnner) => {
            setTimeout(() => resolveInner(), 2000);
        })
    );
    reject();//ここでrejectを呼んでも状態は変化しない。
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//Promise { <state>: "fulfilled", <value>: undefined }
//Promise { <state>: "fulfilled", <value>: undefined }
//...
```
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve(//呼ばれた時点でresolvedになり引数↓のPromiseにロックインされる。
        new Promise((resolveInner, rejectInnner) => {
            setTimeout(() => resolveInner(20), 2000);
        })
    );
    resolve(10);//ここの10は影響を与えない。
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//Promise { <state>: "fulfilled", <value>: 20 }
//Promise { <state>: "fulfilled", <value>: 20 }
//...
```


ただし、**resolved**になることと処理が止まることは別問題です。
```javascript
const promise1 = new Promise((resolve, reject) => {
    resolve();//この時点でresolvedになりfulfilled(settled)。
    console.log("このコメントは出力される");//無効になるわけではない。
});
console.log(promise1);
//このコメントは出力される
//Promise { <state>: "fulfilled", <value>: undefined }
```

# 参考）コンストラクタについて

`new`により`Promise`オブジェクトを返します。

```javascript
new Promise(executor)
```
`executor`は関数オブジェクトであり、`resolveFunc`と`rejectFunc`の2つの引数を持ちます。通常`executor`は以下のようになります。

```javascript
function executor(resolveFunc, rejectFunc) {
    //処理
}
```
より具体的に記載すると、例えば以下のようなコードになります。`executor`はアロー関数にしています。つまりコンストラクタは`resolveFunc`と`rejectFunc`の2つの引数を持つわけではなく、`resolveFunc`と`rejectFunc`の2つの引数を持つ`executor`を引数に持つということになります。
```javascript
const promise1 = new Promise((resolve, reject) => {
    try{
        //何らかの処理
        resolve(value);//処理が成功した場合resolveFunc(ここではresolve)を呼ぶ
    }catch(reason){
        reject(reason);//処理が失敗した場合rejectFunc(ここではreject)を呼ぶ
    }
});
```
<!--
1. `executor`について
   * 関数オブジェクトです。
   * 戻り値は無視されます（`return`自体が無視されるわけではないです）。
   * `Promise`が構築されたとき同期的に呼ばれます。

2. `resolveFunc`と`rejectFunc`について
   * 関数オブジェクトです。
   * 名前は任意に設定できます。
   * コンストラクタにより自動で作成されるので自分で定義する必要はありません。
   * 引数を1つだけ受け付けます（任意の型で可）。
   * `rejectFunc`は処理が失敗したときに呼ばれるため、引数は通常`Error`オブジェクトになります。  
-->

# 参考URL

* [Promise - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) （[日本語](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise)）  
* [Promise() constructor - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) （[日本語](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise)）  
* [ECMAScript® 2024 Language Specification](https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promise-objects)