ES2017の新機能「async / await」でPromise(非同期処理)をスッキリ書く

いよいよECMAScript(ES2017 / ES8)のリリースが来月(2017年6月)に迫ってきました。すでに仕様は固まり、あとは承認を待つだけの状態となっているようです。ES2017の目玉機能のひとつが、今回紹介する「async/await」です。Promiseを使った処理をすっきり書くことができるようになります。async/awaitはすでに多くのモダンブラウザでは使えるようになっており、すでに利用している方もいるかと思いますが、ES2017のリリースも間近なことですし、改めて予習の意味もこめて今回async/awaitの使い方などについて紹介していきます。

はじめに

JavaScriptの長年の課題として、非同期処理をいかに書くかというものがあります。かつては「コールバック地獄」という言葉が巷で溢れかえっていました。非同期処理の結果をコールバック関数の引数に渡し、そのコールバック関数の結果を、新たなコールバック関数の引数に渡し、さらに…というコールバック関数の連鎖によりネストが深くなり、しまいには視認性の悪いコードになっていくことをコールバック地獄と呼びました。

そのコールバック地獄を解決するべく、jQueryではjQuery.Deferred、ES2015ではPromiseといった技術が発明されました。これらの技術により、非同期処理と結果処理を別々に分けて書くことができるようになり、これまでと比べればだいぶ見通しの良いコードを書くことができるようになりました

Promiseで非同期処理を書く

では、ES2015のPromiseを用いて非同期処理を書くとどうなるでしょうか。非同期処理とその結果を受けた一連の処理を書くと以下のようになります。

// Promiseを作成
function foo(num, callback) {
  return new Promise((resolve, reject) => {
    return callback(resolve, reject, num);
  });
}

// 非同期処理
function bar(resolve, reject, num) {
  return setTimeout(() => {
    try {
      if (num) {
        return resolve(num + num);
      }
      return reject('error');     
    } catch(err) {
      return reject(err);
    }
  }, 100);
}

// 非同期処理の結果を用いた処理
function baz() {
  const a = foo(1, bar);
  a.then(
    res => foo(res, bar) // => 1 + 1
  ).then(
    res => foo(res, bar) // => 2 + 2
  ).then(
    res => console.log(res) // => 4 + 4 
  ).catch(
    err => console.error(`error: ${err}`)
  );
}

// baz()関数を実行
baz(); // => 8

このようにPromiseを使うと、上記のコードのように「非同期の処理」、「結果の処理」といったように処理を分けて書くことができ、それぞれの役割も把握しやすくなります

ただし、ここで気になる点としては、.then()メソッドの連鎖の部分でしょうか。上記のコードに関しては、特に.then()メソッドに渡したコールバック関数に余計なことはさせていないので、それほど複雑さは感じないかもしれません。しかし、この.then()メソッドの連鎖の部分は、処理が多くなると複雑になっていきます。全てがチェーンのように繋がっているので、一度実装したコードを修正する際は、骨が折れることが予想されます。

そこでこのPromiseをよりシンプルに書けるようにするために、ES2017に追加されることになったのが「async/await」です。

async/awaitで非同期処理を書く

async/awaitとは

「async/await」は、async関数と、await演算子のことを言います。await演算子はPromiseの処理が完了するのを待機するために使うもので、async関数(async function)内でのみ有効となります。

MDNでは、async関数とawait演算子に関して、以下のように説明しています。

async function が呼び出された場合、promise を返します。async function が値を返した場合、promise は返された値で解決されます。async function が例外や何らかの値をスローした場合、promise はスローされた値で拒否されます。

Async function は、await 式を含むことできます。await 式は、async function の実行を一時停止し、promise の解決を待ちます。そして、async function の実行を再開し、解決された値を返します。

おそらくこれだけだと何のことやらわかりにくいかと思いますので、実際にコードを書いてどのような挙動をするか見ていきたいと思います。

async/awaitで非同期処理を実装する

実際に上記のPromiseのコードをasync/awaitを使って書き換えると以下のようになります。上記のコードの.then()メソッドの部分の複雑さが解消されて、だいぶスッキリするのがわかるでしょう。

// Promiseを作成
function foo(num, cb) {
  return new Promise((resolve, reject) => {
    return cb(resolve, reject, num);
  });
}

// 非同期処理
function bar(resolve, reject, num) {
  return setTimeout(() => {
    try {
      if (num) {
        return resolve(num + num);
      }
      return reject('error!!!!!!');     
    } catch(err) {
      return reject(err);
    }
  }, 100);
}

// 非同期処理の結果を用いた処理(async/await)
async function baz() {
  try {
    const a1 = await foo(1, bar); // => 1 + 1
    const a2 = await foo(a1, bar); // => 2 + 2
    const a3 = await foo(a2, bar); // => 4 + 4
    return console.log(a3); // => 8
  } catch(err) {
    return console.error(`error: ${err}`);
  }
}

// async関数の実行
baz().catch(
  err => console.error(`error: ${err}`)
); // => 8

コードを見てもらえればわかるかと思いますが、非同期処理の結果はそれぞれ変数に格納され、同期的に処理を書いていくことができます。これは、Promiseを返す関数(ここではfoo関数)を呼び出す際に、await演算子をつけることで、async関数の実行を一時停止し、promiseの解決を待ってくれるようになるからです。コールバック関数も書かなくて済みます。

また、以下のようにasync関数では、別の非同期処理をawaitさせることもできたり、awaitawaitの間に同期的に処理を書いていくこともできます。

// Promiseを作成
function foo(num, cb) {
  return new Promise((resolve, reject) => {
    return cb(resolve, reject, num);
  });
}

// 非同期処理(100ms)
function bar(resolve, reject, num) {
  return setTimeout(() => {
    try {
      if (num) {
        return resolve(num + num);
      }
      return reject('error');     
    } catch(err) {
      return reject(err);
    }
  }, 100);
}

// 別の非同期処理(500ms)
function bar2(resolve, reject, num) {
  return setTimeout(() => {
    try {
      if (num) {
        return resolve(num + num + num);
      }
      return reject('error');     
    } catch(err) {
      return reject(err);
    }
  }, 500);
}

// 非同期処理の結果を用いた処理(async/await)
async function baz() {
  try {
    const a1 = await foo(1, bar); // => 1 + 1
    const a2 = await foo(a1, bar2); // => 2 + 2 + 2
    const a3 = a1 + a2; // => 2 + 6
    const a4 = await foo(a3, bar); // => 8 + 8
    return console.log(a4); // => 16
  } catch(err) {
    return console.error(`error: ${err}`);
  }
}

// async関数の実行
baz().catch(
  err => console.error(`error: ${err}`)
); // => 12

awaitはPromise.allも待ってくれる

async/awaitは、Promise.allにも同様に機能してくれます。上記のコードのasync関数の部分を以下のように変更しました。await演算子をつけて、Promise.allを呼び出すと、async関数の実行を一時停止し、その処理結果を待ってくれることがわかるかと思います。

async function qux() {
  try {
    const a1 = await Promise.all([foo(1, bar), foo(2, bar2), foo(3, bar)]);
    console.log(a1[0]); // => 1 + 1
    console.log(a1[1]); // => 2 + 2 + 2
    console.log(a1[2]); // => 3 + 3
    const a2 = await foo(1, bar); // 1 + 1
    return console.log(a1[0] + a1[1] + a1[2] + a2) // => 2 + 6 + 6 + 2
  } catch(err) {
    return console.error(`error: ${err}`);
  }
}

// async関数の実行
qux().catch(
  err => console.error(`error: ${err}`)
); // => 16

Promise.allの結果は、配列となります。

エラーハンドリング(例外処理)

非同期処理を書くときは、エラーのハンドリング(例外処理)が欠かせません。async/awaitでは、例外処理をtry {} catch {}構文を使って同期的な処理を書くときと同じように書くことができます。

説明するより、実際のコードを見た方がわかりやすいかと思いますので、以下にいくつかのエラーハンドリングの例を挙げておきます。基本的にエラーがどこで発生しても、try-catchで実装しておけばエラーをキャッチしてくれます

await演算子外でエラーが発生した場合

// async関数
async function foo() {
  try {
    throw new Error('エラー');
    const a = await (() => {
      return new Promise(
        (resolve, reject) => setTimeout(() => {
          try {
            return console.log(resolve('OK'));
          } catch(err) {
            return reject(err);
          }
        }, 100)
      );
    })();
    return console.log(a);
  } catch(err) {
    return console.error(`async関数内catch: ${err}`); // => async関数内catch: エラー
  }
}

// async関数の実行
foo().catch(
  err => console.log(`foo().catch: ${err}`)
);

await演算子内の同期処理中にエラーが発生した場合

// async関数
async function foo() {
  try {
    const a = await (() => {
      throw new Error('エラー');
      return new Promise(
        (resolve, reject) => setTimeout(() => {
          try {
            return console.log(resolve('OK'));
          } catch(err) {
            return reject(err);
          }
        }, 100)
      );
    })();
    return console.log(a);
  } catch(err) {
    return console.error(`async関数内catch: ${err}`); // => async関数内catch: エラー
  }
}

// async関数の実行
foo().catch(
  err => console.log(`foo().catch: ${err}`)
);

await演算子内の非同期処理中にエラーが発生した場合

// async関数
async function foo() {
  try {
    const a = await (() => {
      return new Promise(
        (resolve, reject) => setTimeout(() => {
          try {
            throw new Error('エラー');
            return console.log(resolve('OK'));
          } catch(err) {
            return reject(err);
          }
        }, 100)
      );
    })();
    return console.log(a);
  } catch(err) {
    return console.error(`async関数内catch: ${err}`); // => async関数内catch: エラー
  }
}

// async関数の実行
foo().catch(
  err => console.log(`foo().catch: ${err}`)
);

なお、async関数は、戻り値としてPromiseを返すようになっているため、try-catch文を使っていない場合など、うまくasync関数内でエラーをキャッチできなかった場合は、Promiseが失敗となるため、Promiseの.catch()メソッドでエラーをキャッチさせることも可能です

// async関数
async function foo() {
  const a = await (() => {
    throw new Error('エラー');
    return new Promise(
      (resolve, reject) => setTimeout(() => {
        try {
          return resolve('OK');
        } catch(err) {
          return reject(err);
        }
      }, 100)
    );
  })();
  return console.log(a);
}

// async関数の実行
foo().catch(
  err => console.log(`foo().catch: ${err}`)
);// => foo().catch: エラー

例外処理についての詳細は以下の記事などを見てもらうとわかりやすいかと思います。

ブラウザ対応状況

async/awaitのブラウザ対応状況は2017年5月現在以下の通りとなっています。

async/ await のブラウザ対応状況
Async functions – Can I use…

async/awaitは、上記の通りブラウザの対応は進んでいますが、残念ながら全てのブラウザで実装がなされていません。従って、状況に応じて以下のツールを使ってコードをトランスパイルする必要があります。

Babelでasync/awaitを使う場合

Babelを使用している場合は、babel-plugin-syntax-async-functionsというpluginを使ってトランスパイルすることで、async/awaitに対応させることができます。

以下のコマンドでbabel-plugin-syntax-async-functionsをインストールします。

npm install --save-dev babel-plugin-syntax-async-functions

.babelrcファイルに以下を記述します。

{
  "plugins": ["syntax-async-functions"]
}

詳細はBabelの公式ドキュメントにてご確認ください。

まとめ

今回記事で説明してきたように、async/awaitは、Promise(非同期処理)の処理をスッキリ書けるようになり、コード全体の見通しを良くすることができるようになります。メンテナンスもしやすくなり、バグを起こすリスクも減らせるようになるかと思います。JavaScriptにとって、非同期処理は切っても切り離せないものです。async/awaitが全てを解決してくれるとは思いませんが、使う頻度が多くなる機能だと思っています。ES2017のリリースに備えて、改めて仕様を押さえておきたいところです。

以下は、ES2015以降の構文に対応したオライリーの初心者向けJavaScript本です。今回紹介したasync/awaitについての解説も載っていますので、ぜひこちらも参考にしてみると良いかと思います。他のES2017に追加される機能も紹介されており、しばらくは手元に1冊置いておきたいJS本ではないでしょうか。

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発
  • 『初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発』
  • Ethan Brown (著), 武舎 広幸 (翻訳), 武舎 るみ (翻訳)
  • 出版社: オライリー・ジャパン
  • 発売日: 2017年1月20日

コメント一覧

  • 必須

コメント