🌴

Promiseメソッドの挙動をまとめてみる

2022/09/07に公開

皆さん、こんにちは!
株式会社エムアイ・ラボの第2回目の記事として、今回はPromiseメソッドの挙動についてまとめていきたいと思います。

Promiseとは

Promiseの各メソッドを紹介する前に、Promiseについて簡単に紹介したいと思います。

Promiseとは非同期処理において、処理が完了もしくは失敗した結果を返すオブジェクトです。Promiseを使用することで、ある処理が実行されてから終了されるのを待つことなく、別の処理を行うことができるという利点があります。


同期/非同期の概念

Promiseの仕組みが必要になってくる理由は、同期/非同期の概念にあります。様々なプログラミング言語には同期処理と非同期処理といった大まかな分類があります。

処理が同期であるということは、プログラムのコードが書かれた順番に、一度に一つのことだけが起こるということを意味します。もしある関数Aの処理が別の関数Bの処理結果に依存する場合、関数Aの処理が終了して結果を返すまで、関数Bの処理を待つ必要があります。

JavaScriptはシングルスレッドでしか動かないため、各スレッドは一度に一つのタスクしか実行することしかできません。

Task A --> Task B --> Task C --> Task D 



そのため、非同期処理を使用することで、ある処理が実行されてから終了結果を待つことなく、別の処理も行うことできます。同期処理と比べて効率的にタスクを処理することができ、それを実現するためにPromiseオブジェクトが使用されています。
(ちなみにPromiseの概念自体はJavaScriptで発見されたものではなく、E言語で発見されたもので並列/並行処理におけるデザインの一種の様です。)

Task A --> Task B --> Task C
Task D --> Task E --> Task F



Promiseの状態

Promiseは以下のいずれかの状態を保持しています。

  • 待機(Pending): 初期値 new Promiseによりインスタンスが生成された時の初期状態です
  • 履行(Fullfilled): 初期値から処理が完了(resolve)した場合
  • 拒否(Rejected): 初期値から処理が失敗(reject)した場合

処理を実行することで初期値である待機状態(Pending)から、履行(Fullfilled)または何らかの理由でエラーとなった拒否(Rejected)の状態に変化することになります。その後、後述するthenメソッドによって処理を連鎖させることができます。

const promise = new Promise((resolve, reject) => {
   setTimeout(() => {
      resolve();
   }, 100);
});

promise.then(() => {
    console.log('Process is done');
  },(error) => {
    console.log('Process Error');
  });



プロミスの連鎖

Promiseはthen、catch、finallyメソッドといったメソッドが用意されています。
Promiseオブジェクトはあらかじめ用意されているメソッド以外は使用することができないため、基本的なやり方は統一されています。統一されたインターフェイスを使用することで、複雑な非同期処理であってもパターン化させることができます。
これらのメソッドはプロミスを返すので、thenメソッドなどを使用して処理を連鎖させることができます。

// ResolvedまたはRejectedのPromiseインスタンスがランダムで入る。
const promise = Math.random() < 0.5 ? Promise.resolve() : Promise.reject();

promise.then(() => {
    // thenメソッド
    console.log("Success");
    
}).catch((error) => {
         // catchメソッド
    console.log("Error");
    
}).finally(() => {
    // finallyは成功、失敗どちらの場合でも呼び出されます。
    console.log("finally");

});



Promiseメソッド

さて前置きが長くなりましたが、Promiseのメソッドについて詳しくまとめていきたいと思います。
PromiseはPromise.race、Promise.all、Promise.allSettled、Promise.anyの4つのメソッドがあり、Promiseの処理結果によって、挙動が変わります。



Promise.anyメソッド

構文: Promise.any(iterable);

Promise.anyメソッドは渡されたオブジェクトのうち、Promiseが1つでも履行(Fullfilled)されると、すぐにその結果をPromiseで返します。渡されたオブジェクトで履行されず、全て拒否された場合、AggregateErrorという、個々のエラーをグループ化したオブジェクトを返します。


サンプルコード

1つでも履行された場合は、すぐにPromiseを返しcompleteが流れます。catchは拾われません。

const promise1 = Promise.reject('reject1');
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'resolve2'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'resolve3'));

const promises = [promise1, promise2, promise3];

Promise.any(promises).then(
  (value) => {
    console.log(value);
   }
 ).catch((error) => {
   console.log(error);
}).finally(() => {
  console.log('complete');
});

// 結果
// "resolve2"
// "complete"



一方で全てRejectedされた場合はcatchにてAggregateErrorが返却され、最終的にfinallyとなります。

const promise1 = Promise.reject('reject1');
const promise2 = new Promise((resolve,reject) => setTimeout(reject, 100, 'resolve2'));
const promise3 = new Promise((resolve,reject) => setTimeout(reject, 500, 'resolve3'));

const promises = [promise1, promise2, promise3];

Promise.any(promises).then(
  (value) => {
    console.log(value);
   }
 ).catch((error) => {
   console.log(error);
}).finally(() => {
  console.log('complete');
});

// 結果
// AggregateError: All promises were rejected
// "complete"


Promise.raceメソッドとの違い

Promise.raceメソッドと同じく、タスクの完了を待つのは一緒です。異なる点としては、raceメソッドがいずれかのタスクが成功または失敗した時点で終了するのに対して、anyメソッドはいずれかのタスクが成功(resolve)した時のみ終了するのが特徴です。



Promise.raceメソッド

構文:Promise.race(iterable);

Promise.raceメソッドは渡されたオブジェクトのうち、最初の1つが履行(Fullfilled)または、拒否(Rejected)された場合、その結果をPromiseで返します。

配列の中の一番最初の結果によって返すPromise結果が変わってくるのが特徴です。
配列の中で一番最初にSettledとなったPromiseがFulfilledの場合は、新しいPromiseインスタンスもFulfilledになります。一方で、配列の中で一番最初にSettledとなったPromiseがRejectedの場合は、新しいPromiseインスタンスも Rejectedになります。


サンプルコード

最初にPromiseがFulfilledとなったタイミングでfinallyのcompleteが流されます。

const firstPromise = new Promise((resolve, reject) => {
       setTimeout(resolve, 300, '成功1');
});

const secondPromise = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, '成功2');
});

Promise.race([secondPromise, firstPromise]).then(
  (value) => {
  console.log(value);
  }).catch((error) => {
  console.log(error);
  }).finally(() => {
  console.log('complete');
});

// 結果
// "成功2"
// "complete"



一番最初にSettledとなったPromiseがRejectedの場合でも、finallyのcompleteが流されます。結果はRejectedですが、catchされません。Fulfilled、Rejectedに関わらず一番最初にSettledとなったPromiseを返す為、一番最初の結果が欲しい場合に使用するメソッドになるかと思います。

const firstPromise = new Promise((resolve, reject) => {
       setTimeout(reject, 300, '失敗1');
});

const secondPromise = new Promise((resolve, reject) => {
    setTimeout(reject, 100, '失敗2');
});

Promise.race([secondPromise, firstPromise]).then(
  (value) => {
    console.log(value);
  }).catch((error) => {
  console.log(error);
  }).finally(() => {
  console.log('complete');
});

// 結果
// "失敗2"
// "complete"


Promise.allメソッドとの違い

Promise.allメソッドは複数のPromiseが全て完了するまで結果を返すのを待ちます。
Promise.raceメソッドは渡されたPromiseが一つでも完了したら(つまりSettled状態となったら)次の処理に移るという違いがあります。



Promise.allSettledメソッド

構文: Promise.allSettled(iterable);

Promise.allSettledメソッドは、履行(Fulfilled)、拒否(Rejected)に関わらず引数で渡されたすべてのオブジェクトを実行して、それぞれの結果を配列で返す特徴があります。


サンプルコード
const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, '成功1');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(reject, 100, '失敗1');
});

Promise.allSettled([promise2, promise1]).then(
  (value) => {
  console.log(value);
}).catch((error) => {
  console.log(error);
}).finally(() => {
   console.log('complete');
});

// 結果
// Array [Object { status: "rejected", reason: "失敗1" }, Object { status: "fulfilled", value: "成功1" }]
// "complete"



返値として、statusが履行(Fulfilled)した場合はvalueが存在し、statusが拒否(Rejected)された場合は、reasonが存在することになります。

[
    {
        "status": "rejected",
        "reason": "失敗1"
    },
    {
        "status": "fulfilled",
        "value": "成功1"
    }
]


使い所

各Promiseの処理に対して、個別にエラーハンドリングしたい場合にPromise.allSettledメソッドを使用できるかと思います。



Promise.allメソッド

構文: Promise.all(iterable);

Promise.allメソッドは渡されたオブジェクトの処理が全て終了した場合、それぞれの結果を配列で返します。1つでも拒否(Rejected)されると、一番最初に拒否(Rejected)されたエラーメッセージが返却されます。


サンプルコード

全てresolveされた場合、thenの処理が行われます。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, '成功1');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, '成功2');
});

Promise.all([promise2, promise1]).then(
  (value) => {
    console.log(value);
}).catch((error) => {
  console.log(error);
}).finally(() => {
   console.log('complete');
});

// 結果
// ['成功2', '成功1']
// "complete"



1つでもrejectされた場合、 一番最初にrejectされたオブジェクトがcatchの処理で行われます。

const promise1 = new Promise((resolve, reject) => {
    setTimeout(reject, 500, '失敗1');
  });

  const promise2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, '成功2');
  });

  Promise.all([promise2, promise1]).then(
    (value) => {
    console.log(value);
  }).catch((error) => {
    console.log("エラー内容:", error);
  }).finally(() => {
     console.log('complete');
  });
  
  // 結果
  // "エラー内容: 失敗1"
  // "complete"


使い所

Promise.allメソッドの特徴として、渡されたオブジェクトの中で1つでも失敗したらcatchが流れることが挙げられます。エラーが出ない前提で確実に全ての処理を成功させたいときに使用すると良いかと思います。ただfinallyは必ず流れるため、POSTメソッド等を実施した際に、catchの処理が流れたとしてもresolveされたものに関してはメソッドの処理が流れているので注意が必要です。



参考文献

https://jsprimer.net/

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise

Discussion