async/awaitを利用したPromise.then処理のリファクタリング


はじめに

Node.js 8.7系以降になると今までPromiseのthen関数を利用して書いていた処理をasync/awaitを利用してスッキリ書くようにできます。この記事はそのリファクタリングの例を紹介するサンプルです。

まず前提として、Promiseが理解できているものとします。Promiseについては、Promiseの本など非常に良いドキュメントがそろっているのでそちらを読まれるとよいのではないかと思います。

単純に言うとPromiseは未来の結果を抽象化したもので、適用したい関数をthen関数を利用して適用することで、未来の結果を関数で変換した未来の結果を得ることができます。

そのためPromiseで結果が遅延して取得されるような関数の結果の合成というのは今まで、then関数を利用して書いてきました。

またthen関数は関数型プログラミング言語でよくあるflatMap関数としての機能を持ち合わせているので、Promiseで多重にくるまっているような結果でもそのPromiseを剥いて一重のPromiseにしてくれます。

Promise.thenを利用した関数getAnswerの実装

ここでは、1秒後に遅れてPromiseして結果が取得されるf1~f4の関数を考えて、(((f1の結果 + f2の結果) * f3の結果) - f4の結果)を求めることができる関数getAnswerを作ることを考えてみましょう。もちろんエラー処理も考慮します。

'use strict';

// Promiseで遅延して取得される関数f1~f4の結果を使って (((1 + 2) * 3) - 4) = 5 を求める

const f1 = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 1000);
});

const f2 = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve(2), 1000);
});

const f3 = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve(3), 1000);
});

const f4 = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve(4), 1000);
});

// 合成する関数の実装、合成時の処理が毎回異なるのでPromise.allしたものを使いずらい
const getAnswer = () => f1()
  .then((v1) => f2().then((v2) => v1 + v2))
  .then((v1_2) => f3().then((v3) => v1_2 * v3))
  .then((v1_2_3) => f4().then((v4) => v1_2_3 - v4));

getAnswer().then((a) => console.log(a)).catch((e) => console.error(e)); // 5

こんな感じに書かれます。これで無事合成したPromiseを取得する getAnswer関数が実装できています。

const getAnswer = () => f1()
  .then((v1) => f2().then((v2) => v1 + v2))
  .then((v1_2) => f3().then((v3) => v1_2 * v3))
  .then((v1_2_3) => f4().then((v4) => v1_2_3 - v4));

ただし、Promiseチェーンでthen関数を重ねていくため、どうしても見た目がゴテゴテしてた感じになってしまいます。Scalaなどだったらfor式を利用して結構スッキリかくことができるのですが、どうもつらいです。

また今回は合成時の処理がそれぞれ異なるため、Promise.allを使って複数のPromiseをドカッと合成するというようなことができません。

async/awaitを利用したリファクタリング

これをasync/awaitを利用して書くと以下のような感じになります。

'use strict';

// Promiseで遅延して取得される関数の結果を使って (((1 + 2) * 3) - 4) = 5 を求める

const f1 = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 1000);
});

const f2 = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve(2), 1000);
});

const f3 = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve(3), 1000);
});

const f4 = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve(4), 1000);
});

// 合成する関数の実装
const getAnswer = async () => (await f1() + await f2()) * await f3() - await f4();

getAnswer().then((a) => console.log(a)).catch((e) => console.error(e)); // 5

得られる結果も処理も全く同じですが、getAnswer関数は、

const getAnswer = async () => (await f1() + await f2()) * await f3() - await f4();

以上のように恐ろしくスッキリかけたことがわかると思います。この差はすごいです。

async/awaitについては、詳しくはMDNのドキュメントを見るとよいとは思いますが、async関数は、自動的にPromise内のresolve関数を呼んで同期処理的な実装でもPromiseを返してくれると関数です。

そして、async関数内のawaitキーワードは、async関数内でPromiseを取得する関数の前にawaitを付けることで、同期処理的な記述の結果を取得できます。

もちろんasyncが返すPromiseはエラーの処理も内包したPromiseとなるため、f1関数がエラーを返すような

const f1 = () => new Promise((resolve, reject) => {
  setTimeout(() => reject('fail'), 1000);
});

だった場合にも正しく合成されて、fail と表示されます。
素晴らしいですね。

おわりに

まだ現在はNode.jsのLTSである6.11.x系ではasync/awaitを利用することができませんが、いずれ8系がLTSになったら満を持してasync/awaitを利用したPromise.then処理のリファクタリングをしていくことができますので、ぜひ利用してコードをスッキリとさせていくとよいのではないかと思います。

あとasync/awaitを理解するためには、まずはPromiseをしっかり理解しておかなくてはならないので、この手の非同期処理やPromiseの合成などがあまりしっくり来ていない人は慣れるためにPromiseの処理に慣れてみるのもひとつかもしれません。

またチームの中にPromiseの理解が浅い人がいたりすると、async/await自体が何をやっているのか理解が難しくなってしまうので、その際にはPromiseの本を利用したPromise実装の演習を行ってみるとよいのではないかと思います。