async/await入門 第1回 async/awaitとは

ES2017としてリリースされる予定になっているasync/awaitは非同期処理を同期的処理のように書くことができる新しい記法です。第1回目はanync/awaitの働きの概要を解説します。

発行

著者 山田 順久 フロントエンド・エンジニア
async/await入門 シリーズの記事一覧

はじめに

async/awaitはJavaScriptで非同期処理を扱う新しい方法です。これまでは、非同期処理を書くためにコールバック関数を使った方法とPromiseを使った方法がありました。

それぞれ、いつ完了するか不明な非同期処理の結果をハンドリングするための関数を別途定義していましたが、async/awaitを使うと、まるで単なる同期的な処理を書いているように非同期処理を表現することができます。

次のコードはasync/awaitを使った一例です。対応ブラウザであれば、これをそのままコピーして開発者ツールのコンソールに貼り付けて実行できます。

async function showGitHubOrganizationReposCount(name) {
  const res = await fetch(`https://api.github.com/orgs/${name}`);
  const org = await res.json();
  console.log(org.public_repos);
}

showGitHubOrganizationReposCount('codegrid');

GitHub APIを使って特定のOrganizationのリポジトリ数を取得する。

fetch()はPromiseを返すAPIです。そのため、本来であればPromiseのthen()メソッドを使って結果を受け取る必要のある場面ですが、ここでは同期処理のような表現でPromiseの解決値を変数に代入しています。

今回は、従来の非同期処理を振り返りながらasync/awaitの働きを見ていきたいと思います。

仕様背景・対応環境

まずは、仕様背景と対応環境です。

async/await(仕様上ではAsync function)は、すでにECMAScript仕様*のFinished Proposalsに含まれており、ES2017としてリリースされる予定になっています。

*注:ECMAScriptの仕様策定プロセス

ECMAScriptの仕様策定プロセスについては、次の記事にも詳しく載っています。

また、執筆時(2017年5月)の対応状況は次のとおりです。

想定する読者

このシリーズとしての想定は、async/awaitを理解したい、あるいは理解を補強したいJSエンジニア向けに記事を書いています。

前提として、async/awaitはPromise*をベースにしたものなので、もしPromiseの知識に不足を感じたら、過去の記事でPromiseについて扱っているので、そちらもあわせて読んでみてください。

*注:Promise

Promiseついては、過去掲載した次の記事で解説をしています。

従来の非同期処理の課題

コールバック関数を使った方法には、関数がネストしていってコードが読みづらくなってしまう、いわゆるコールバック地獄*という状況がありました。コールバック関数を使う場合には、そうならないよう気を付ける手間があったり、各関数ごとにエラーハンドリングをしてやらないといけない煩わしさがありました(async.jsのようなフロー制御ライブラリである程度解決する問題ではありますが)。

*注:コールバック地獄

コールバック地獄とは、コールバック関数の中に沢山のコールバック関数が追記されて入れ子になっていった結果、直感的によみづらいコードとなってしまう様子のことです。日本に限らず英語圏でもcallback hellという言葉で知られています。

ではここからは、解説のために、架空のニュースサイトのWeb APIから情報を取得するという例で話を進めていきます。

コールバック関数のケース

次のコードは架空のニュースサイトのAPIを使って記事IDから著者の情報を取得する例です*。

*注:コールバック関数の第一引数

見慣れないコードと思われる場合のため補足しておくと、Node.jsを用いている人の慣習ではコールバック関数の第一引数にエラーオブジェクトを渡します。

function fetchAuthorByEntry(entryId, callback) {
  fetchEntry(entryId, (err, entry) => {
    if (err) {
      // fetchEntry() のエラーハンドリング
      return callback(err);
    }
    // コールバック関数の中でさらに呼び出されるコールバック関数
    fetchAuthor(entry.authorId, (err, author) => {
      if (err) {
        // ここで fetchAuthor() のエラーハンドリングも
        // しないといけない
        return callback(err);
      }
      callback(null, author);
    });
  })
}

最初に、引数として受け取った記事IDを使って、記事APIから情報を取得するところから処理が始まります。その中に含まれる著者のIDを使って、さらに著者APIから情報を取得します。APIへのリクエストごとにコールバック関数はネストしていて、その度にエラーハンドリングも行わなければなりません。

この調子で整理することもなくコールバックを足していってしまうと、コールバック地獄を見ることになってしまいます。

Promiseを使った例

次は、この例で使っている機能がすべて、fetchEntry()fetchAuthor()もPromiseを返すケースだったらというコードです。

function fetchAuthorByEntry(entryId) {
  return fetchEntry(entryId)
    .then(entry => fetchAuthor(entry.authorId))
    .catch(err => Promise.reject(err));
  })
}

Promiseチェインによってコールバックのネストは一段階に抑えられました。また、例外の返却はPromiseチェイン末尾のcatch()で行え、さらにArrow Functionによるreturnの省略が行えることもあり、この時点でもだいぶすっきりしたコードになっています。

async/awaitを使った例

こうしてPromiseで整理されたコードにasync/awaitを使うと、より把握しやすいコードになります。次のコードは、同じ処理をasync/awaitを使ったものに置き換えた例です。コールバック関数も、Promiseチェインもない同期的な表現になっています。

fetchEntry()fetchAuthor()は前の例と同じくPromiseを返す前提としています。

async function fetchAuthorByEntry(entryId) {
  try {
    const entry = await fetchEntry(entryId);
    const author = await fetchAuthor(entry.authorId);
    return author;
  } catch(err) {
    throw err;
  }
}

ひと目見て、次のようなPromise特有の記述がなくなっていることがわかります。

  • Promiseのthen()メソッドがなくなった
  • Promiseのcatch()メソッドがなくなった(例外をtry-catchで捕捉している)

そして、今回のテーマであるasync/awaitにまつわる記述が加わっています。

  • asyncという記述がfunctionの前に増えている
  • awaitという記述が式の間に挟まっている

次節では、このasyncawaitについて解説していきます。

asyncの働き

先の例では、function文の頭にasyncというキーワードが追加されていました。これは仕様に沿った言い方をするとAsync functionを定義していることになります。

async function fetchAuthorByEntry(entryId) {
  try {
    const entry = await fetchEntry(entryId);
    const author = await fetchAuthor(entry.authorId);
    return author;
  } catch(err) {
    throw err;
  }
}

Async functionの中では、awaitを使うことができます。もし、asyncでない普通のfunctionの中でこれを使おうとするとシンタックスエラーとなります。

ここまでのまとめとしては次のとおりです。

  • asyncを使ってAsync functionを定義できる
  • Async functionの中でだけawaitを使える

awaitの働き

さて、Async functionの中だけで使えるawaitとはどのような働きがあるのでしょうか。

awaitは、その後に続けて記述されたPromiseオブジェクト内で行われている処理が完了するまで、Async function内の処理をその場で一時停止します。

そして、Promise内の処理が完了すると、一時停止していたAsync function内の処理を再開します。そのときに変数の代入を行う必要があれば、Promiseの解決値を取り出して変数への代入も行います。

ちょっと例え話で考えてみましょう。日常の家事には、さまざまなPromise的なものがあります。たとえば、洗濯です。洗濯機のスイッチを押すと、洗濯、すすぎ、脱水あるいは乾燥までを行い、音で完了を知らせます。

しかしながら、私たちは洗濯をしている間ずっとその場で洗濯機が洗濯を終えるのを待つなんてことはありません。

次の例では、洗濯機のrun()メソッドは成果物である洗濯物を持っていることを期待されたPromiseオブジェクトを返します。

家事の中における洗濯タスクをPromiseで表現した例

function 家事() {
  洗濯機.run()
    .then(洗濯物 => カゴ.push(洗濯物))
    .then(...)
}

上記の例でも洗濯機を回した後は掃除機をかけるなどして、洗濯が終わったらthen()メソッドに続きが書いてあるように、取り込みを始めるでしょう。

でも、洗濯機を買ったばかりで、どうしても洗濯機が洗濯を終えるまで、他の家事などせずに、その場で待ちたいとしましょう。

このコードをasyncawaitを使って書き換えてみます。

洗濯の完了をawaitで待つ例

async function 家事() {
  const 洗濯物 = await 洗濯機.run();
  カゴ.push(洗濯物);
  ...
}

この場合はawaitの働きによって、洗濯機が洗濯を終えるまで、家事をストップさせてその場で待つことになります。そして、awaitは洗濯機のrun()メソッドが返すPromiseオブジェクトから洗濯物を取り出し、これに洗濯物という変数名を割り当てます。

プログラムの出来事を現実に例えることには限界がありますが、awaitによって、あるAsync function内の処理が一時停止すること、Promiseオブジェクトの解決値を取り出すことについては、イメージしてもらえたでしょうか。

エラーハンドリングについては、Promise特有のcatch()メソッドを使ったものではなく、普通に同期処理を書く場合と同様のtry-catchを使ったエラーハンドリングを使用できます。

ここまでのまとめです。

  • awaitはAsync function内でのみ使える
  • awaitはPromiseの完了をその場で待つ
  • awaitはPromiseの完了時に持っている値を取り出し変数への代入を行う
  • 例外処理はtry-catchで行う

まとめ

今回はasync/awaitの紹介ということで、Promiseを同期的に表現できるという利点をまず取り上げてみました。今後の記事では、asyncawaitそれぞれの挙動をさらに掘り下げ、async/awaitの活用例などを紹介していく予定です。