9
10

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 5 years have passed since last update.

JavaScriptのPromiseに関するまとめ(async/await含む)

Posted at

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

確認環境

Windows 10
Chrome DevTools 73
Sources>snippets にて確認しました。

Promiseの基本

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

JavaScript Promiseの本
Promiseを使う | MDN

1箇所からデータを取得する

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

// 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...');
    });
}
実行結果
> 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を実行します。

// 通信成功し、データ(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
Promise.prototype.then() | MDN
Promise.prototype.catch() | MDN
Promise.prototype.finally() | MDN

2箇所からデータを受け取る処理を直列でつなげる

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

直列処理
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...');
    });
}
実行結果
> 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の状態

3つの状態

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

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

一度状態がfulfilledまたはrejectedに変化すると、もう変わることはありません。
いずれかに変化済みの状態はsettledと呼ばれます。
そのため、一つのオブジェクトのthencatchメソッドに登録したコールバック関数は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秒に設定されるようになっています。

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);
    });
}
実行結果
> 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同様に配列で処理を渡し、並列で実行されるのを待機するのですが、いずれか一つが解決または拒否の結果を出せばそれを採用し、後続処理を実行します。

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);
    });
}
実行結果
> 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の本によると、タイムアウト処理の実装に利用できることを知りました。
従来からAjax通信時に利用されてきたXMLHttpRequest(XHR)には、標準でタイムアウトの機能があります。
XHRの後続であるFetchAPIには、タイムアウト機能がありません。(2019年3月現在)
もしかしたら今後は出番が増えていくのかもしれませんね。

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

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

いろいろなメソッドチェーン

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

単純な直列

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

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】
    })
}
実行結果
> testPromise(5)
resolve → hoge → fuga → piyo → fulfilled

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

ラップされたように見えるチェーン

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

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】
    });
}
実行結果
> testPromise(5)
resolve → innerThen → outerThen

> testPromise(4)
reject → innerCatch → outerThen

再帰関数de直列

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

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`);
    }
  ]
));
実行結果
>
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を利用します。

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);
    });
}
実行結果
> 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
await | MDN

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

参考情報

【javascript】Promiseについて学ぶ①
Promiseについて0から勉強してみた
【JavaScript】ちゃんと理解しておきたいPromiseの勘所など

9
10
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
9
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?