1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-03

はじめに

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

状態遷移図

Promiseには排他的な3つの状態があります。

  • pending : 初期および待機状態。fulfilled でも rejected でもない状態。
  • fulfilled : 処理が完全に成功した状態。
  • rejected : 処理が失敗した状態。
    states.png
  • 全体として、正確さよりも理解のしやすさを優先して表現しています(実際に理解しやすくなっているかは別ですが)。また、状態確定など、この記事内でしか出てこない単語もあるため参考URLと見比べながら理解しようとすると混乱する可能性があります。
  • unresolvedresolvedlocked insettledについては説明用であり、「Promisesetteledになる」というような言い方はしても、Promiseの状態が実際にsetteledになる(pendingfulfilledrejected以外の状態になる)ことはありません。
  • unresolvedresolvedについてはStatesではなくFatesと表現されており、またfulfilledsettledit is fulfilledit is settledなどのように記載されているのに対し、locked inについてはit has been "locked in"と記載されています。そのため、これら(3つの状態以外)を同じように扱うのは好ましくないかもしれませんが、概要把握を優先し状態として扱っています。

イメージの説明

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

  • 処理の全てを自分で行う場合

    • 処理がすべて正常に動作した場合 → 処理は完全に成功(「ファイルの中身を読み取ってその内容を返す処理」を行うという約束は履行された)
      unresolved → (b) → fulfilled
    • 処理が途中で失敗した場合 → 処理失敗(「ファイルの中身を読み取ってその内容を返す処理」を行うという約束は拒否された)
      unresolvedrejected
  • ファイルのパスの取得だけを自分で処理し、パスからファイルの中身を読み取って内容を返す処理は別のPromiseの処理とした(別のPromiseに任せた)場合

    • パスの取得が成功し、別の処理も成功した場合 → 処理は完全に成功
      unresolved →(a)→ locked in(別の処理実行中) → 別の処理成功で fulfilled
    • パスの取得は成功したが、別の処理が失敗した場合 → 処理失敗
      unresolved →(a)→ locked in(別の処理実行中) → 別の処理失敗で rejected
    • パスの取得が失敗した場合 → 処理失敗
      unresolvedrejected

サンプルコード

resolve関数の引数がプリミティブの場合(数値や文字列など)やthenableでない場合は、直ぐにfulfilledになります。

  • thenableについての詳細はこちらを参照。つまりPromiseオブジェクトと似たようなオブジェクトのことでPromiseオブジェクトもthenableになります。よくわからない場合、まずはthenablePromiseオブジェクトに置き換えて読んでみてください。
const promise1 = new Promise((resolve, reject) => {
    resolve(10);
});
console.log(promise1);
//Promise { <state>: "fulfilled", <value>: 10 }
const promise1 = new Promise((resolve, reject) => {
    resolve(console);
});
console.log(promise1);
//Promise { <state>: "fulfilled", <value>: Console }

resolve関数の引数がthenableの場合は、そのthenableの状態に依存します。thenablefulfilledになると、自身もfulfilledになります。

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 }
//...

thenablerejectedになると、自身もrejectedになります。

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

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になります。

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になったわけでもないです*

const promise1 = new Promise((resolve, reject) => {
    resolve({
        get then() {
            throw new Error("error");
        },
    });
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: Error }

reject関数が呼ばれると、直ぐにrejectedになります。

const promise1 = new Promise((resolve, reject) => {
    reject();
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: undefined }
//Uncaught (in promise) undefined

例外が発生すると、直ぐにrejectedになります。

const promise1 = new Promise((resolve, reject) => {
    throw new Error('error');
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: Error }
//Uncaught (in promise) Error: error

処理するものがなくなっても成功(または失敗)にはなりません。以下はずっとpendingのままです。

const promise1 = new Promise((resolve, reject) => {
    //処理がないため即終了
});
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//...

一度処理が成功もしくは失敗した(resolvedになった)後に、resolve関数やreject関数が呼ばれたり、例外が発生したりしても、状態に影響を与えることも結果の値が変わることもありません。影響するのは処理が成功も失敗もしていないとき(unresolvedのとき)に呼ばれたり、発生したりしたときです。

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 }
//...
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になることと処理が止まることは別問題です。

const promise1 = new Promise((resolve, reject) => {
    resolve();//この時点でresolvedになりfulfilled(settled)。
    console.log("このコメントは出力される");//無効になるわけではない。
});
console.log(promise1);
//このコメントは出力される
//Promise { <state>: "fulfilled", <value>: undefined }

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

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

new Promise(executor)

executorは関数オブジェクトであり、resolveFuncrejectFuncの2つの引数を持ちます。通常executorは以下のようになります。

function executor(resolveFunc, rejectFunc) {
    //処理
}

より具体的に記載すると、例えば以下のようなコードになります。executorはアロー関数にしています。つまりコンストラクタはresolveFuncrejectFuncの2つの引数を持つわけではなく、resolveFuncrejectFuncの2つの引数を持つexecutorを引数に持つということになります。

const promise1 = new Promise((resolve, reject) => {
    try{
        //何らかの処理
        resolve(value);//処理が成功した場合resolveFunc(ここではresolve)を呼ぶ
    }catch(reason){
        reject(reason);//処理が失敗した場合rejectFunc(ここではreject)を呼ぶ
    }
});

参考URL

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?