JavaScriptのPromiseの使い方・書き方

2021年3月14日JavaScript

JavaScriptのPromiseについて、基礎的な内容をまとめました。
Promiseをより直感的に記述できるasync/awaitに関しては次の記事を参考にしてみてください。


fetch()メソッドについては、次の記事を参考にしてみてください。

 

Promiseについて

Promiseは、ES6からの機能で非同期処理をより簡単に可読性が上がるように書けるようにしたものです。
Promiseは、非同期操作の将来の結果のプレースホルダーとして使われるオブジェクトです。
言い換えると、Promiseは、非同期で運ばれる値のための容器です。
さらに言い換えると、Promiseは、AJAX通信からレスポンスされる将来の値(future value)のための容器です。

Promiseを使う2つのメリット

1つは、イベントやコールバック関数に非同期の結果を制御させずにすみます。これらに任せると、ときに予期せぬ結果が出たりすることがあります。
もう1つは、コールバック関数をネストする代わりに、Promiseをチェーンして非同期処理のシークエンスをつくることができます。コールバックヘル(callback hell)を回避することができます。

Promiseのライフサイクル

将来の値が利用可能になるまで、ペンディング(pending)されます。この間も非同期処理は行われます。そして、非同期のタスクの結果が決定されたら(settled)、次の処理のどちらかが行われます。

fulfilled(要件を満たした状態)もしくはrejected(要件を満たさなかった状態)

fulfilledは成功したとき、rejectedは、非同期処理中にエラーが発生したときです。
これらの状態をコードで制御することができます。
Promiseは、一度非同期処理の結果をsettledしたら、その状態は変わることはありません。

Consume promise(promiseの活用)

Promiseの結果を得るには、これらの状態を得ることがとても便利になります。それをconsume a promise(直訳すると約束を消費する)といいます。consume a promiseというのは、たとえばfetch APIから返されたPromiseがすでにあるときなどです。

Build promise(promiseの生成)

Promiseはconsumeされる前に、まず最初にビルドされていないといけません。(Build promise)
このprimiseの生成は、promiseを返すfetch APIなどです。多くの場合、コードを書くときにすることはprimiseをconsumeするのみになります。

Promiseの書き方

new Promise(function (resolve, reject) {
  resolve('実引数'); //同期処理
}).then(function(仮引数){
  resolveのときの処理内容; //非同期処理
}).catch(function(仮引数){
  rejectのときの処理内容; //非同期処理
}).finally(function() {
  終了処理の内容; //非同期処理
});

new演算子を使って、Promiseをインスタンス化します。
そして、then(),catch(),finally()メソッドを使って非同期に対して制御します。
Promiseはコールバック関数を持ち、resolve, rejectの2つの引数を持ちます。
resolveが呼ばれた場合then()メソッドに移り、rejectが呼ばれた場合catch()メソッドが実行されます。
finally()メソッドはresolveでもrejectでも実行されます。
resolveは正常に動作したとき、rejectはなんらかのエラーが起きたときに渡される引数です。
また、Promiseチェーンとは、Promiseを使って非同期処理を順番に実行することです。
チェーンする場合は、then()にプロミスのインスタンスを必ずれreturnすることです。

Promiseコンストラクタ

Promiseコンストラクタは、プロミスに対応していない関数をラップする目的で使います。
Promiseコンストラクタは、引数を一つとります。それをexecutor(関数)といいます。
Promiseが実行されるとき、即座にexecutorを実行します。
このexecutorは2つの引数をとります。resolveとrejectです。


const kuji = new Promise(function (resolve, reject) {
  if (Math.random() >= 0.5) {
    //この条件がfulfilledの状態になったら、resolve()が実行される
    resolve('あたりです!');
  } else { //この条件がrejectedな状態なら、reject()が実行される
    reject('はずれです');
  }
});

kuji.then(res => console.log(res))
.catch(err => console.error(err));

上のように記述すると、値がfulfilledのとき、resolveがresに渡されます。
値がrejectedな状態なら、reject()がerrに渡されます。

Promiseのrejectedの処理

Promiseのrejectedの処理には、いつくかの処理方法があります。

then()メソッドの第二引数

まずは、then()メソッドで処理する方法です。


.then(response => response.json(), console.error(`${err}`))

then()の第二引数には、エラー(rejected)のときの処理を書きます。
エラーの処理を操作することをhandle errorsもしくはcatch errorsといいます。then()メソッド内でrejectedを捉えることができるためcatchという単語も使われます。

catch()メソッド


.catch(err => console.error(`${err}:エラー`));

then()の第二引数で書くと、非同期処理がいくつもある場合、第二引数に毎回書かないといけないのは非効率です。
そこで、catch()メソッドを使います。
catch()メソッドは、チェーンの最後に書きます。そうすることで、どこでエラーが置きてもエラー時の処理をすることが可能になります。

throw new Errorで自作のエラーメッセージをつくる


    .then(response => {
      
      if(!response.ok)
      throw new Error(`エラーが発生しました (${response.status})`)
      
      return response.json();
    })
    .then(...)
    .then(...)
    .catch(err => console.error(`${err}:エラー`))

throwはreturnと同じように即座に結果を返します。
new Error()はコンストラクタ関数です。
それが、自作のエラーのメッセージを下のcatch()メソッドがあるところまで伝搬されることができます。

Promiseと並列処理-Promise.all()

Promise.all(iterable) メソッドを使うことで、非同期処理を並列で処理することができるようになります。
iterableとは反復可能なデータのことです。
反復可能なオブジェクトでPromiseのインスタンスをいれます。
よく使われるのは配列です。
Promise.all()が反復可能なオブジェクトである配列に格納されたPromiseのインスタンスがすべて完了するまで次のthen()メソッドを待つことができます。

Promise.all()の書き方

Promise.all([非同期処理1, 非同期処理2, ...])//反復可能なオブジェクトでPromiseのインスタンスをいれる
  .then((result) => {
    // 全ての非同期処理が成功した場合
  })
  .catch((err) => {
    // いづれかの非同期処理が失敗した場合
  });

全ての非同期処理が成功した場合には、Promise.all()の配列部分の完了する時間は別々であってもそれらが完了した後に、then()メソッドで次の処理をすることができます。

いづれかの非同期処理が失敗した場合にはcatch()が実行されます。

また、データは配列になって渡されます。

また、Promiseチェーンの中でPromise.all()を使うことも可能です。

Promise.all()の具体例

function time(val) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(val++);
      resolve(val);
    }, val * 300);
  });
}
Promise.all([time(1), time(2), time(3), time(4)]).then(function (data) {
  console.log('end');
  console.log(data);//(7) [2, 3, 4, 5]
});

Promise.race()

Promise.race()は いづれかの非同期処理が実行されたら、次のthen()メソッドに以降します。
データは、最初に実行される値が返されます。
次の例では、一番最初に実行されるtime(1)が実行された後、他の非同期処理を待たずにthen()メソッドが呼ばれて、そのあとval++で1増えるので、2が返されます。

function time(val) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(val++);
      resolve(val);
    }, val * 300);
  });
}
Promise.race([time(1), time(2), time(3), time(4)]).then(function (data) {
  console.log('end');
  console.log(data);//2
});

Promise.allSettled()

Promise.allSettled()は、status(fulfilledかrejected)と値を格納したオブジェクトを配列にしてデータを返します。
Promise.all()と同じくすべての非同期処理を実行した後に、then()メソッドを実行します。
Promise.all()との違いは、Promise.all()はいづれかの処理がrejectになった場合、catch()に移行しますが、
Promise.allSettled()は、rejectになった場合もstatusで表示するだけで、catch()メソッドには移行しません。

function time(val) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(val++);
      resolve(val);
    }, val * 300);
  });
}
Promise.allSettled([time(1), time(2), time(3), time(4)]).then(function (data) {
  console.log('end');
  console.log(data);
  // (4) [{…}, {…}, {…}, {…}]
  // 0: {status: "fulfilled", value: 2}
  // 1: {status: "fulfilled", value: 3}
  // 2: {status: "fulfilled", value: 4}
  // 3: {status: "fulfilled", value: 5}
});

JavaScriptのXMLHttpRequestとPromiseの違いについては、次の記事を参考にしてみてください。
JavaScriptのXMLHttpRequestとPromiseの違い