JavaScriptのPromiseに関するまとめ（async/await含む）

JavaScriptのPromiseについて、これまでに学んだことをまとめました。
async/awaitもfetchも扱っているものはPromiseなので、JavaScriptの非同期処理に於いて、基礎のような存在と言えなくもない？

# 確認環境
Windows 10
Chrome DevTools 73
Sources＞snippets にて確認しました。

# Promiseの基本
他の方も紹介されていますが、Promiseの基礎知識についてはこちらのサイトが大変参考になります。

[JavaScript Promiseの本](http://azu.github.io/promises-book/)
[Promiseを使う | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises)

## 1箇所からデータを取得する
`setTimeout`を非同期通信処理に見立てて、一つのデータを受け取り検査します。
奇数を受け取ったら通信成功、偶数もしくは数値と評価できない値の場合は通信失敗です。

```js
// Promiseオブジェクトを作成して返す
function createPromise(param) {
  // インスタンス作成時にコールバック関数を渡します。
  return new Promise((resolve, reject) => {
    // 非同期通信処理を定義
    setTimeout((result) => {
      console.log('Result:', result);
      // 非同期通信で受け取った値 result を検査する
      if (result % 2) {
        // 成功と判断した際は1番目に受け取った関数を実行
        resolve(result);
      } else {
        // 失敗と判断した際は2番目に受け取った関数を実行
        reject(`${result} is not odd`);
      }
    }, 500, (param | 0));
  });
}

function testPromise(param){
  // 作成されたPromiseオブジェクトを使って、非同期通信後の処理を定義する
  createPromise(param)
    .then((result) => {
      // 成功時の後続処理。resolve実行時に渡された値を受け取る
      console.log('Hello Promise!:', result);
    })
    .catch((error) => {
      // 失敗時の後続処理。reject実行時に渡された値を受け取る
      console.log('Error:', error);
    })
    .finally(() => {
      // 成功・失敗、いずれの場合も最後に実行したい処理、引数は受け取らない
      console.log('Fin...');
    });
}
```

```console:実行結果
> testPromise(5)
Result: 5
Hello Promise!: 5
Fin...

> testPromise(4)
Result: 4
Error: 4 is not odd
Fin...
```

Promise関数を使ってPromiseオブジェクトを作成します。
この際、結果を待ちたい非同期処理を定義したコールバック関数`executor`を渡します。
この`executor`は、引数として二つの関数を受け取ります。
一つ目の関数`resolve`は、非同期処理が解決（成功）したと判断した場合に実行します。
二つ目の関数`reject`は、非同期処理が拒否（失敗）したと判断した場合に実行します。
それぞれ、後続処理に渡したいデータを引数に渡します。
例えば、HTTPのレスポンスコードが`HTTP 200 OK`でも、受け取った中身が不正の為に失敗と判断したい場合は`reject`を実行します。

```js
// 通信成功し、データ(responseText)を取得することは出来た。が、まだ油断は出来ない…
try {
  const data = JSON.parse(responseText || null);
  if (data === null) {
    reject('データが無いよ');
  } else {
    // 受け取ったJSONデータを無事にパースできた
    resolve(data);
  }
} catch {
  reject('JSONをパース出来なかったよ');
}
```

作成したPromiseオブジェクトは3つのメソッドを持ちます。それぞれに引数で解決時・拒否時・完了時に実行するコールバック関数を渡します。
各メソッドの戻り値はPromiseオブジェクトなので、メソッドチェーンで繋げることが可能です。
`then`は、解決時（`executor`内で`resolve`実行時）に呼び出される処理`onFulfilled`を一つ目の引数にとります。**※**
`catch`は、拒否時（`executor`内で`reject`実行時）に呼び出される処理`onRejected`を引数にとります。
また、`executor`内でエラーが発生した場合も`onRejected`が呼ばれます。
`finally`は、解決・拒否にかかわらず、`onFulfilled`または`onRejected`が実行された後に実行される処理`onFinally`を引数にとります。

**※** `then`は、二つ目の引数に`onRejected`を受け取ります。これは省略が可能です。`catch`を利用せずに、`then`に二つの関数を渡すことも可能です。
`catch`を利用した場合との違いは、`onFulfilled`内でエラーが発生した場合、`then`に渡した`onRejected`は実行されませんが、`catch`に渡した`onRejected`は実行されます。

[Promise | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise)
[Promise.prototype.then() | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/then)
[Promise.prototype.catch() | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch)
[Promise.prototype.finally() | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally)

## 2箇所からデータを受け取る処理を直列でつなげる
ある場所から取得したデータをもとに、また別の場所へリクエストを投げたい場合は、Promiseオブジェクトを作成する処理をメソッドチェーン内で繰り返します。
一つ目の`then`内で再びcreatePromise関数を呼び出し、新たに作成したPromiseオブジェクトを戻り値に設定しています。

```js:直列処理
function createPromise(param) {
  return new Promise((resolve, reject) => {
    setTimeout((result) => {
      console.log('Result:', result);
      if (result % 2) {
        resolve(result);
      } else {
        reject(`${result} is not odd`);
      }
    }, 500, (param | 0));
  });
}

function testPromise(param) {
  // Promiseオブジェクトを作成して最初の非同期処理Aを実行
  createPromise(param)
    .then((result) => {
      // 処理Aが解決した場合の後続処理
      // 処理Aから受け取った値を加工して次の非同期処理に利用する
      const newNum = (result / 3) | 0;

      console.log(`Next: ${result} to ${newNum}`);

      // 新たなPromiseオブジェクトを作成して非同期処理Bを実行
      return createPromise(newNum);
    })
    .then((result) => {
      // 処理Bが解決した場合の後続処理
      console.log('Hello Promise!:', result);
    })
    .catch((error) => {
      // 処理Aまたは処理Bが拒否した場合の後続処理
      console.log('Error:', error);
    })
    .finally(() => {
      console.log('Fin...');
    });
}
```

```console:実行結果
> testPromise(5)
Result: 5
Next: 5 to 1
Result: 1
Hello Promise!: 1
Fin...

> testPromise(7)
Result: 7
Next: 7 to 2
Result: 2
Error: 2 is not odd
Fin...
```

Promiseオブジェクトを作成すると`resolve`,`reject`の使い捨てバケツを渡され、それを使って次に荷物を受け渡す。
受け取った側は再びPromiseオブジェクトを作成してバケツを入手し、次に荷物を受け渡す。
そんなイメージでしょうか。。。

# Promiseの状態
## ３つの状態
作成したPromiseオブジェクトは、下記三つのうちいずれかの"状態"にあります。

|状態|条件|条件成立時の挙動|
|:--|:--|:--|
|pending (初期状態)|作成時|待機|
|fulfilled (解決済み)|resolve()の実行|onFulfilledを呼ぶ|
|rejected (拒否済み)|reject()の実行|onRejectedを呼ぶ|

一度状態が`fulfilled`または`rejected`に変化すると、もう変わることはありません。
いずれかに変化済みの状態は`settled`と呼ばれます。
そのため、一つのオブジェクトの`then`や`catch`メソッドに登録したコールバック関数は1回しか呼ばれません。
故に使い捨てバケツであり、直列接続をするには毎回新しいPromiseを作成する必要があります。

## 静的メソッドのresolveとreject
静的メソッドの`Promise.resolve()`, `Promise.reject()`を利用すると、最初から`settled`の状態のPromiseオブジェクトを作成できます。
`then()`や`catch()`内では、`pending`のPromiseオブジェクトを返すとその結果を待ちますが、`settled`のPromiseオブジェクトを返すとその状態に応じて後続処理が実行されます。（非同期）
Promiseオブジェクト以外のものは`fulfilled`のPromiseオブジェクトにラップされ、後続の`then()`へ進みます。
後続の`catch()`へ進みたい場合は`rejected`のオブジェクトを返す必要があり、`Promise.reject()`を利用して作成します。
`executor`内でこのメソッドを利用しても、`executor`を実行したPromiseオブジェクト自体の状態は変わらないため、後続処理が実行されることはありません。
状態を変えたいオブジェクトから渡された`resolve`,`rejected`を利用する必要があります。

# Promise.allで複数箇所からデータを受け取る処理を並列でつなげる
取得順序は問わないが全て揃ったら次のステップに進みたい、そんなあなたのニーズにお答え出来るのが、静的メソッドの`Promise.all`です。

createPromise関数の処理内容は上記と同一ですが、setTimeout関数の待機時間をランダムで1～5秒に設定されるようになっています。

```js:Promise.all
function createPromise(param) {
  const sec = Math.floor(Math.random() * 5 + 1) * 1000;

  return new Promise((resolve, reject) => {
    setTimeout((result) => {
      console.log('Result:', result, `(${sec}ms)`);
      if (result % 2) {
        resolve(result);
      } else {
        reject(`${result} is not odd`);
      }
    }, sec, (param | 0));
  });
}

function testPromise(param1, param2, param3) {

  // 作成したPromiseオブジェクトの配列を渡す
  Promise.all([
    createPromise(param1),
    createPromise(param2),
    createPromise(param3),
    'MoiMoi'
  ])
    .then(([first, second, third, forth]) => {

      // all()に渡した全ての処理が解決した場合、実行される
      // 取得順に関係なく、all()に渡した順番の配列で結果を受け取る
      console.log('Complete!:', first, second, third, forth);
    })
    .catch((error) => {

      // いずれか一つでも拒否すると1回だけ実行される
      console.log('Error:', error);
    });
}
```

```console:実行結果
> testPromise(3,5,7)
Result: 5 (1000ms)
Result: 3 (3000ms)
Result: 7 (4000ms)
Complete!: 3 5 7 MoiMoi

> testPromise(1,2,3)
Result: 2 (1000ms)
Error: 2 is not odd
Result: 1 (2000ms)
Result: 3 (3000ms)

> testPromise(9,4,6)
Result: 4 (1000ms)
Error: 4 is not odd
Result: 9 (3000ms)
Result: 6 (5000ms)
```

`all()`に渡す配列には、Promiseオブジェクト以外のものを要素にすることが出来ます。上記では3番目に文字列を渡しています。
この場合解決済みのPromiseとしてラップされ、後続の`then`で値を取得できます。
一度拒否が発生すると、その後に実行された`resolve()`,`reject()`は無視されます。

# Promise.raceで一番最初に出た結果を採用する
## 概要
`Promise.all`に良く似た並列処理で、`Promise.race`があります。
`all`同様に配列で処理を渡し、並列で実行されるのを待機するのですが、いずれか一つが解決または拒否の結果を出せばそれを採用し、後続処理を実行します。

```js:Promise.race
/**
 * createPromise関数は上記 Promise.all のサンプルコードと同じ
 */

function testPromise(param1, param2, param3) {
  // Promise.all と同様に配列を渡す
  Promise.race([
    createPromise(param1),
    createPromise(param2),
    createPromise(param3)
  ])
    .then((result) => {
      // 取得できるのは一つ
      // 一番最初に帰ってきた結果を採用する
      console.log('Win!:', result);
    })
    .catch((error) => {
      console.log('Error:', error);
    });
}
```

```console:実行結果
> testPromise(2,7,3)
Result: 7 (2000ms)
Win!: 7
Result: 2 (3000ms)
Result: 3 (3000ms)

> testPromise(2,7,3)
Result: 2 (4000ms)
Error: 2 is not odd
Result: 7 (5000ms)
Result: 3 (5000ms)
```

## タイムアウト処理を実装する

当初、使いどころが良く分からなかったのですが、[4.5. Promise.raceとdelayによるXHRのキャンセル | JavaScript Promiseの本](https://azu.github.io/promises-book/#race-delay-timeout)によると、タイムアウト処理の実装に利用できることを知りました。
従来からAjax通信時に利用されてきた`XMLHttpRequest(XHR)`には、標準でタイムアウトの機能があります。
`XHR`の後続である`FetchAPI`には、タイムアウト機能がありません。（2019年3月現在）
もしかしたら今後は出番が増えていくのかもしれませんね。

```js:Promise.raceによるタイムアウト
// データ取得処理
function fetchWrp(path) {
  // FetchAPIでデータを取得する
  return fetch(path)
    .then((response) => {
      if (response.ok) {
        return response.json();
      } else {
        return Promise.reject(response.status);
      }
    })
    .catch((error) => {
      return Promise.reject(error);
    })

  // DevToolでの実行用テストコード
  // 1～6秒後に結果を返す
  /*
  const sec = Math.floor(Math.random() * 6 + 1) * 1000;
  console.log(`will take ${sec}ms...`);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`fetched (${sec}ms)`);
    }, sec)
  });
  */
}
// タイムアウト処理
function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(`timeout (${ms}ms)`);
    }, ms);
  });
}

// timeoutに指定した時間内にfetchWrpの結果が出なければ、タイムアウト
Promise.race([
  fetchWrp('/sample/api/data/'),
  timeout(3000)
])
  .then((result) => {
    console.log('Result:', result);
  })
  .catch((error) => {
    console.log('Error:', error);
  });
```

[Fetch API |MDN](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)

**FetchAPIでreject**
`fetch()`は実行するとPromiseオブジェクトを返すので、`then`や`catch`を利用できます。
取得失敗時は`Promise.reject`を利用することで、`Promise.race`の`catch`へ処理が進むようにしています。

# いろいろなメソッドチェーン
実務で利用機会があるか否かは別として、理解を深める為に書いたサンプルコードです。

## 単純な直列
下記の例では、`executor`内で拒否されて【A】へ進んでも、その後は【B】、【C】と続き、【D】が実行されることはありません。
メソッドチェーンの途中で、`Promise.reject()`の結果を返すと、後続の`catch()`が実行されます。

```js:SampleA
function testPromise(param) {
  const num = param | 0;

  new Promise((resolve, reject) => {
    if (num % 2) {
      resolve('resolve');
    } else {
      reject('reject');
    }
  })
    .then((res) => {
      return `${res} → hoge`;
    })
    .then((res) => {
      return `${res} → fuga`;
    })
    .catch((res) => {
      return `${res} → boo`; //【A】
    })
    .then((res) => {
      return `${res} → piyo`; //【B】
      // return Promise.reject(res); とした場合、【D】に進む
      // resolve → hoge → fuga → rejected
    })
    .then((res) => {
      console.log(`${res} → fulfilled`); //【C】
    })
    .catch((res) => {
      console.log(`${res} → rejected`); //【D】
    })
}
```

```console:実行結果
> testPromise(5)
resolve → hoge → fuga → piyo → fulfilled

> testPromise(4)
reject → boo → piyo → fulfilled
```

## ラップされたように見えるチェーン
関数で分割されていますが、挙動としては上記のSampleAと同じです。
【B】で解決済みのPromiseオブジェクトが返されることになる為、続いて【C】が実行され、【D】は実行されません。

```js:SampleB
function createPromise(param) {
  const num = param | 0;
  return new Promise((resolve, reject) => {
    if (num % 2) {
      resolve('resolve');
    } else {
      reject('reject');
    }
  })
    .then((res) => {
      return `${res} → innerThen`; //【A】
    })
    .catch((res) => {
      return `${res} → innerCatch`; //【B】
    });
}

function testPromise(param) {

  createPromise(param)
    .then((res) => {
      console.log(`${res} → outerThen`); //【C】
    })
    .catch((res) => {
      console.log(`${res} → outerCatch`); //【D】
    });
}
```

```console:実行結果
> testPromise(5)
resolve → innerThen → outerThen

> testPromise(4)
reject → innerCatch → outerThen
```

## 再帰関数de直列
予め用意した、取得データ検査用の関数のリストを渡して、再帰で順次実行します。

```js:SampleC
(function testPromise(fncAry, param) {
  const doSomething = fncAry.shift();

  if (!doSomething) {
    return;
  }

  new Promise((resolve, reject) => {
    setTimeout(doSomething, 500, param, resolve, reject);
  })
    .then((res) => {
      console.log('Then:', res);
      testPromise(fncAry, res);
    })
    .catch((res) => {
      console.log('Catch:', res);
    });
}(
  [
    // 1番目
    (param, resolve, reject) => {
      resolve('hoge');
    },
    // 2番目
    (param, resolve, reject) => {
      const rum = Math.floor(Math.random() * 10 + 1);
      console.log('Number is ' + rum);
      
      if (rum % 2) {
        resolve(`${param} → fuga`);
      } else {
        reject(`${param} → booo`);
      }
    },
    // 3番目
    (param, resolve, reject) => {
      resolve(`${param} → piyo`);
    }
  ]
));
```

```console:実行結果
>
Then: hoge
Number is 9
Then: hoge → fuga
Then: hoge → fuga → piyo

>
Then: hoge
Number is 4
Catch: hoge → booo
```

# Promiseさん家のasync・await兄弟
es2017で`async/await`が登場しました。
`then()`のチェーンを使うことなく、直列処理を実現できます。
`async`関数内で`await`式を使うと、そこで処理を一時停止します。
再開するには、`await`式に渡されたPromiseオブジェクトが解決済みの状態になる必要があります。
再開時、`await`式はPromiseにラップされていた値を返します。つまり`onFulfilled`の引数で受け取る値と同じものが返されます。
Promiseオブジェクト以外のものを渡すと、その値をラップした解決済みのPromiseオブジェクトに変換される為、そのまま処理が続行します。
ただし`await`以降の実行は非同期です。

`async`関数の戻り値はPromiseオブジェクトなので、`then`,`catch`,`finally`メソッドが利用可能です。
挙動としては、上記のSampleBと似ています。
並列処理はこれまでどおり`Promise.all`を利用します。

```js:async/await
function createPromise(param) {
  return new Promise((resolve, reject) => {
    setTimeout((result) => {
      // 値が0以下は却下
      if (result > 0) {
        resolve(result);
      } else {
        reject(`result:${result} is rejected`);
      }
    }, 500, (param | 0));
  });
}

async function testAsync(param) {
  // 1番目
  const paramA = await createPromise(param);
  console.log(paramA);
  // 奇数は却下
  if (paramA % 2) {
    // Promiseの静的関数の結果を返してcatchに進む
    return Promise.reject(`paramA：${paramA} is rejected`);
  }

  // 2番目
  const paramB = await createPromise(paramA * 2);
  console.log(paramB);
  // 3の倍数は却下
  if (!(paramB % 3)) {
    // エラーを発生させて終了させる
    throw new Error(`paramB：${paramB} is rejected`);
  }

  // 3番目
  // paramBよりrumが大きいとcreatePromise内で却下される
  const rum = Math.floor(Math.random() * 10 + 1);
  console.log(`${paramB} minus ${rum}`);
  const paramC = await createPromise(paramB - rum);
  console.log(paramC);

  // 4番目
  // 解決済みとしてそのまま処理が続行（非同期）
  const paramD = await 'wait a moment';
  console.log(paramD);

  // thenに進む
  return 'End!';
}

function testPromise(param) {
  testAsync(param)
    .then((res) => {
      // async関数内で戻された値を受け取る
      console.log(res);
    })
    .catch((err) => {
      // async関数内でPromiseの却下もしくはエラーの発行で実行される
      console.log(err);
    });
}
```

```console:実行結果
> testPromise(0)
result:0 is rejected

> testPromise(7)
7
paramA：7 is rejected

> testPromise(6)
6
12
Error: paramB：12 is rejected

> testPromise(2)
2
4
4 minus 5
result:-1 is rejected

> testPromise(8)
8
16
16 minus 2
14
wait a moment
End!
```

[async function | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function)
[await | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/await)



Promiseさんとは仲良くなれましたでしょうか。


# 参考情報
[【javascript】Promiseについて学ぶ①](https://qiita.com/chihiro/items/658e514b709e00a49ac6)
[Promiseについて0から勉強してみた](https://qiita.com/toshihirock/items/e49b66f8685a8510bd76)
[【JavaScript】ちゃんと理解しておきたいPromiseの勘所など](https://kde.hateblo.jp/entry/2018/11/01/042432)