【JavaScript】async と await

async と await JavaScript

async / await とは

async / await 構文は ES8 (ES2017) で導入された構文であり、これを使用することで Promise を使った非同期処理を、より直感的に記述することができる。

async は関数に対して使用するキーワードであり、関数の宣言時に function の前に置いて使用する。

async function myfunc() {
  // 何らかの処理
}

async キーワードで宣言した関数には次のような特徴がある。

  • async で宣言した関数の内部でのみ await 演算子を使用することができる。
  • async で宣言した関数は、戻り値として常に Promise を返却する

以降で、これらの特徴について説明する。

await 演算子

async で宣言した関数の中でのみ使用可能

await 演算子は、async キーワードで宣言された関数の中でのみ使用でき、それ以外の箇所で使用するとエラーになる。

let value = await 100;
// --> Uncaught SyntaxError: await is only valid in async function

Promise インスタンスの解決・拒否を待つ

await 演算子は、それを使用している関数の実行を、次のように停止・再開する。

  • 関数の実行を一時停止し、Promise インスタンスの解決・拒否を待つ。
  • Promise インスタンスの解決により、関数の実行を再開する。
  • Promise インスタンスの拒否により、エラーが投げられる。

また、await 演算子により関数の実行が一時停止すると、プログラムの実行は関数の呼び出し元に戻り、関数呼び出しの直後の処理を続けることになる。

まず、await の後ろの Promise インスタンスを解決する場合のサンプルを示す。

let resolve2;

async function asyncfunc() {
  const promise = new Promise(function(resolve) {
    resolve2 = resolve;
  });

  console.log('await で promise の解決・拒否を待ちます。')
  await promise;
  console.log('promise が解決したので関数の実行を再開します。')
}

asyncfunc();
// --> await で promise の解決・拒否を待ちます。

resolve2();
// --> promise が解決したので関数の実行を再開します。

次に、Promise インスタンスを拒否した場合のサンプルを示す。async 関数の中では、await 演算子が投げたエラーを try…catch 文でキャッチすることができる。

let reject2;

async function asyncfunc() {
  const promise = new Promise(function(resolve, reject) {
    reject2 = reject;
  });

  try {
    console.log('await で promise の解決・拒否を待ちます。');
    await promise;
    console.log('promise が解決したので関数の実行を再開します。');
  } catch (err) {
    console.log('promise が拒否されました。')
  }
}

asyncfunc();
// --> await で promise の解決・拒否を待ちます。

reject2();
// --> promise が拒否されました。

await 演算子による非同期処理化

上記のように await 演算子により、Promise インスタンスが解決・拒否されるまで関数の実行が停止することが分かった。

しかし、この「関数の実行が停止する」という部分に少し違和感を覚えないだろうか? 実行が再開し、関数が終了した場合は呼び出し元に処理が戻るのだろうか?

次の2つのサンプルを試すとその疑問が解消する。

async function asyncfunc() {
  console.log('await 前');
  await 100;
  console.log('await 後');
}

queueMicrotask(function() {
  console.log('job');
})
console.log('asyncfunc 呼び出し前');
asyncfunc();
console.log('asyncfunc 呼び出し後');

// asyncfunc 呼び出し前
// await 前
// asyncfunc 呼び出し後
// job
// await 後
async function asyncfunc() {
  console.log('await 前');
  // await 100;
  console.log('await 後');
}

queueMicrotask(function() {
  console.log('job');
})
console.log('asyncfunc 呼び出し前');
asyncfunc();
console.log('asyncfunc 呼び出し後');

// asyncfunc 呼び出し前
// await 前
// await 後
// asyncfunc 呼び出し後
// job

このサンプルを実行してみると、await 100; の記述直後の ‘await 後’ のコンソール出力が、’job’ より後になっている。

つまり、async 宣言した関数内で await 演算子を記述すると、その部分以降の処理は非同期処理として扱われ、グローバルコンテキスト終了後のイベントループで処理されるということが分かる。

await 演算子が返却する値

参考:6.2.3.1 Await

次に await 演算子が返却する値について調べてみよう。

await に続く値が Promise インスタンスである場合

await に続く値が Promise のインスタンスである場合は、その解決(resolve)を待って、解決したらその結果値(つまり、内部プロパティ [[PromiseResult]] の値)を返却する

async function asyncfunc() {
  const promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello');
    }, 1000);
  });

  console.log('result:', await promise);
}
asyncfunc();
// --> 1秒後に result: Hello と出力される

ただし、Promise インスタンスが拒否(reject)された場合は、結果値を返却する代わりに結果値をスロー(throw)する

async function asyncfunc() {
  const promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
      reject('Hello');
    }, 1000);
  });

  try {
    console.log('result:', await promise);
  } catch (err) {
    console.log('error:', err);
  }
}
asyncfunc();
// --> 1秒後に error: Hello と出力される

await に続く値が Promise インスタンスでない場合

await に続く値が数値や文字列のような Promise のインスタンスではない場合は、その値が返却される

async function asyncfunc() {
  console.log(await 100);
  console.log(await 'Hello');
  console.log(await {a: 1, b: 2});
}
asyncfunc();

// 100
// Hello
// {a: 1, b: 2}

実際には、暗黙的にその値で resolve(解決)された Promise インスタンスに変換した後、その結果値 [[PromiseResult]] を返却している。

async / await で非同期処理を順番に実行する

await 演算子は、それに続く Promise インスタンスの解決・拒否を待ち、解決した後に関数の実行を再開するという性質を持つのだった。

そのため、非同期処理による解決を待つ Promise インスタンスに対して await を使用すれば、その非同期処理の終了(解決)を待ってから次の非同期処理を実行させるという、Promise チェーンも簡単に記述できるようになる。

つまり、非同期処理のコールバック関数の連鎖

// ...
asyncfunc1(function(val1) {
  // ...
  asyncfunc2(function(val2) {
    //...
    asyncfunc3(function(val3) {
      //...
    });
  });
});

は、ES6 (ES2015) から導入された Promise 構文によって、

new Promise(function(resolve) {
  // ...
  asyncfunc1(function(val1a) {
    // ...
    resolve(val1b);
  })
}).then(function(val1c) {
  // ...
  return new Promise(function(resolve) {
    // ...
    asyncfunc2(function(val2a) {
      // ...
      resolve(val2b);
    });
  });
}).then(function(val2c) {
  // ...
  return new Promise(function(resolve) {
    // ...
    asyncfunc3(function(val3a) {
      // ...
    });
  });
});

という形の then メソッドの連鎖(Promise チェーン)で書けるようになったが、さらに ES8 (ES2017) からは async / await 構文の導入により、次のような形で書くこともできるようになった。

async function asyncfunc() {
  // ...
  const val1c = await new Promise(function(resolve) {
    // ...
    asyncfunc1(function(val1a) {
      // ...
      resolve(val1b);
    })
  });
  const val2c = await new Promise(function(resolve) {
    // ...
    asyncfunc2(function(val2a) {
      // ...
      resolve(val2b);
    })
  });
  const val3c = await new Promise(function(resolve) {
    // ...
    asyncfunc3(function(val3a) {
      // ...
      resolve(val3b);
    })
  });
}

例えば、

setTimeout(function() {
  console.log('first');
  setTimeout(function() {
    console.log('second');
    setTimeout(function() {
      console.log('third');
      setTimeout(function() {
        console.log('fourth');
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

このような setTimeout 関数を使用した非同期処理の連続した呼び出しは、Promise 構文を使うと、次のように書ける。

// 1秒後に msg をコンソールに出力して解決する Promise を生成する関数
function consoleOut(msg) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log(msg);
      resolve();
    }, 1000);
  });
}

consoleOut('first')
.then(function() {
  return consoleOut('second');
})
.then(function() {
  return consoleOut('third');
})
.then(function() {
  return consoleOut('fourth');
});

また、async / await 構文を使用すると、次のように書ける。

// 1秒後に msg をコンソールに出力して解決する Promise を生成する関数
function consoleOut(msg) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log(msg);
      resolve();
    }, 1000);
  });
}

async function func() {
  await consoleOut('first');
  await consoleOut('second');
  await consoleOut('third');
  await consoleOut('fourth');
}
func();

一つの非同期処理の終了を待ってから次の非同期処理を実行していくという記述が、async / await を使用すると非常に分かりやすく書けることを実感できる。

async 宣言した関数は常に Promise を返却する

async キーワードで宣言された関数は、必ず Promise のインスタンスを返却する。このことを確認してみよう。

return 文を省略した場合

async を付与しない普通の関数ならば、return 文を省略した場合は undefined が返却される。

一方、async を付与した関数の場合は、関数が最初の await で実行を停止した時点で未解決(”pendeing”)の Promise インスタンスが返却され、関数が終了すると undefined で解決される。

async 宣言した関数に return を記述しなかった場合

return Promise インスタンス以外; の場合

数値や文字列のような Promise のインスタンスではない値を return した場合は、関数が最初の await で実行を停止した時点で未解決(”pendeing”)の Promise インスタンスが返却され、関数が終了すると(return 文が実行されると)return した値で解決される

async 宣言した関数で、Promise  のインスタンス以外を return した場合

return Promise インスタンス; の場合

Promise のインスタンスを return した場合は、関数が最初の await で実行を停止した時点で未解決(”pendeing”)の Promise インスタンスが返却され、関数が終了すると(return 文が実行されると)return したインスタンスと同じ状態になる

async 宣言した関数で、解決された Promise  のインスタンスを return した場合
async 宣言した関数で、拒否された Promise  のインスタンスを return した場合

関数内で例外をスローした場合

async 宣言した関数の中で例外をスローする場合は、関数が最初の await で実行を停止した時点で未解決(”pendeing”)の Promise インスタンスが返却され、例外をスローした時点で、スロー(throw) した値で拒否される

async 宣言した関数で例外をスローした場合

タイトルとURLをコピーしました