JavaScript アドバンス

JavaScript の async と await

1. async と await はプロミスを簡素化する

asyncawait を使用すると、プロミス(Promise)がより扱いやすくなります。
内部的には依然としてプロミスを使用していますが、通常の同期処理のようにステップ・バイ・ステップでコードを記述できるようになります。

  • async は、関数がプロミスを返すようにします。
  • await は、関数がプロミスの解決(Resolve)を待機するようにします。

2. なぜ async と await が存在するのか

プロミスのチェーン(.then() の連結)は、処理が増えるにつれて非常に長くなることがあります。
asyncawait は、ネスト(入れ子)を減らし、コードの可読性を向上させるために作成されました。

プロミスの例

// 順番に実行する3つの関数
function step1() {
  return Promise.resolve("A");
}
function step2(value) {
  return Promise.resolve(value + "B");
}
function step3(value) {
  return Promise.resolve(value + "C");
}

// 3つの関数をステップごとに実行
step1()
.then(function(value) {
  return step2(value);
})
.then(function(value) {
  return step3(value);
})
.then(function(value) {
  myDisplayer(value);
});

同じフローを asyncawait で記述すると、非常に読みやすくなります。

// 3つの関数を順番に実行する関数
async function run() {
  let v1 = await step1();
  let v2 = await step2(v1);
  let v3 = await step3(v2);
  myDisplayer(v3);
}

run();

3. async キーワード

関数の前に async キーワード(Keyword)を付けると、その関数は必ずプロミスを返すようになります。
たとえ通常の値をリターンした場合でも、自動的にプロミスとしてラップされます。

async function myFunction() {
  return "Hello";
}

これは、以下のコードと同じ意味になります:

function myFunction() {
  return Promise.resolve("Hello");
}

返り値はプロミスなので、結果を処理するには .then() を使用します。

myFunction().then(
  function(value) { /* 成功時の処理 */ },
  function(value) { /* エラー時の処理 */ }
);
async function myFunction() {
  return "Hello";
}
myFunction().then(
  function(value) { myDisplayer(value); },
  function(value) { myDisplayer(value); }
);

エラーが発生しない正常なレスポンスのみを期待する場合は、よりシンプルに記述できます。

async function myFunction() {
  return "Hello";
}
myFunction().then(
  function(value) { myDisplayer(value); }
);

4. await キーワード

await キーワードは、プロミスが解決(Resolve)されるまで関数の実行を一時停止し、待機させます。

let value = await promise;

await キーワードは、async 関数の内部でしか使用できません。

function step1() {
  return Promise.resolve("A");
}

async function run() {
  let value = await step1();
  myDisplayer(value);
}

run();

5. try...catch によるエラーハンドリング

プロミスではエラー処理に .catch() を使用しますが、asyncawait では通常の同期コードと同様に try...catch を使用します。

function fail() {
  return Promise.reject("失敗しました");
}

async function run() {
  try {
    let value = await fail();
    console.log(value);
  } catch (error) {
    // await しているプロミスからのエラーはここでキャッチされます
    console.log(error);
  }
}

run();

await しているプロミスから発生したエラーは、通常の例外と同じようにキャッチされます。

6. シーケンシャル(順次) vs パラレル(並列)

一つずつ await すると、タスクはシーケンシャル(順次)に実行されます。
これは、あるステップが前のステップの結果に依存している場合に適しています。

async function run() {
  let a = await step1();
  let b = await step2();
  console.log(a, b);
}

タスク同士が独立している場合は、それらをパラレル(並列)で実行できます。
その際は、Promise.all() を使用して両方の完了を待ちます。

async function run() {
  // プロミスを先に開始させる
  let p1 = step1();
  let p2 = step2();
  
  // 同時に待機する
  let values = await Promise.all([p1, p2]);
  console.log(values);
}

まずプロミスを開始させ、その後にまとめて await するのがコツです。

7. fetch を使った実例

fetch() はプロミスを返すため、asyncawait の格好の例となります。

async function loadData() {
  try {
    let response = await fetch("data.json");
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.log(error);
  }
}

loadData();

これは、プロミスベースの非同期コードを、同期的なスタイルで記述したものです。

8. 初心者がよく陥るミス

  • async 関数の外側で await を使用しようとしてエラーを発生させる。
  • try...catch を忘れてしまい、非同期エラーが握りつぶされたり未処理になったりする。

非同期コードが失敗した場合は、ブラウザのコンソールや Network タブを確認する癖をつけましょう。

9. その他の例

基本的な構文

async function myDisplay() {
  let myPromise = new Promise(function(resolve, reject) {
    resolve("大好きです!!");
  });
  document.getElementById("demo").innerHTML = await myPromise;
}

myDisplay();

2つのパラメータ(resolve と reject)は JavaScript によって事前に定義されています。
私たちはそれらを作成するのではなく、処理の準備が整ったときにいずれかを呼び出します。多くの場合、reject 関数は必要ありません。

reject なしの例

async function myDisplay() {
  let myPromise = new Promise(function(resolve) {
    resolve("大好きです!!");
  });
  document.getElementById("demo").innerHTML = await myPromise;
}

myDisplay();

タイムアウトを待機する例

async function myDisplay() {
  let myPromise = new Promise(function(resolve) {
    setTimeout(function() { resolve("大好きです!!"); }, 3000);
  });
  document.getElementById("demo").innerHTML = await myPromise;
}

myDisplay();