LoginSignup
2
2

More than 3 years have passed since last update.

Promiseのメモ - その4: asyncとawait

Posted at

asyncとawaitの仕様を調べます。EcmaScript 2017(8th Edition)で追加された機能です。

基本的な使い方

awaitは、Promiseオブジェクトを返す関数を並べて実行する、つまり同期を取りつつ実行するのに使うようです。ただし、awaitはasyncを付けた関数の中でしか使えません。

次の例では、Promiseを返すmakePromiseを並べて、前にawaitを付けています。await makePromise(a)の実行が終わってから、await makePromise(b)が順に実行されるはずです。

function makePromise(num) {
  return new Promise((resolve, reject) => {
    if(num % 2 == 0)
      resolve('OK');
    else
      reject('NG');
    console.log(`executer called: ${num}`);
  });
}

(async (a, b) => {
   let x = await makePromise(a);
   let y = await makePromise(b);
   console.log(`async function finished: ${x} ${y}`);
})(2, 4);
console.log("async function called");
結果
executer called: 2
async function called
executer called: 4
async function finished: OK OK

アロー関数が読みにくければ、次のように書いても同じです。

let afunc = async function(a, b) {
  let x = await makePromise(a);
  let y = await makePromise(b);
  console.log(`async function finished: ${x} ${y}`);
};
afunc(2, 4);
console.log("async function called");

普通の関数定義の前にasyncを付けることもできます。

async function afunc(a, b) {
  let x = await makePromise(a);
  let y = await makePromise(b);
  console.log(`async function finished: ${x} ${y}`);
}
afunc(2, 4);
console.log("async function called");

awaitの働き

上記の例では、「executer called: 2」が実行されて少し経ってから「executer called: 4」が実行されています。async関数の中では順番に動いていますが、外側から見ると非同期的な動きです。

awaitは次の働きをしているようです。

  • Promiseオブジェクトの前にawaitを付けると、履行済みになるまで待つ。
  • 「await Promiseオブジェクト」は履行済み結果の値を返す。

thenやcatchはPromiseオブジェクトを返すので、次のようにするとxに"OKOK"が入ります。

let x = await makePromise(a).then(x => x + x);

async関数の中にさえあれば、awaitはどんな式の前にでも付けられるようです。次にようにしてもエラーになりません。zにはtrueかfalseが入ります。

let z = await (a == b);

履行済みになるまで待つので、次のようにresolveを呼ばない場合は、2つ目のmakePromiseはいつまでも実行されません。async関数を呼び出しても、async関数の中身の実行は終了しません。

function makePromise(num) {
  return new Promise((resolve, reject) => {
    console.log(`executer called: ${num}`);
  });
}

(async (a, b) => {
  let x = await makePromise(a);
  let y = await makePromise(b);
  console.log(`async function finished: ${x} ${y}`);
})(2, 4);
console.log("async function called");
結果
executer called: 2
async function called

asyncが返すもの

次のように、asyncを付けて作った関数が返すものと、その関数呼び出しが返すものを調べます。

let afunc = async (a, b) => {
};
console.log(afunc.constructor);
console.log(afunc(2, 4).constructor);
結果(Chrome)
ƒ AsyncFunction() { [native code] }
ƒ Promise() { [native code] }

asyncを付けて作った関数は、FunctionオブジェクトではなくAsyncFunctionオブジェクトになります。Functionと違って、AsyncFunctionではnew AsyncFunction()とすると「ReferenceError: AsyncFunction is not defined」が出ます。つまり、AsyncFunctionという名前は内部的にだけ使われています。

async関数に()を付けて呼び出すと、Promiseオブジェクトが返ります。ということは、async関数の呼び出しに対してthenやcatchが使えるということです。

次の例は、関数makeAsyncPromiseでasyncが作ったPromiseを返し、そのPromiseに対してthenとcatchをつなげています。async関数の戻り値が、履行済み結果の値になることが分かります。

function makePromise(num) {
  return new Promise((resolve, reject) => {
    if(num % 2 == 0)
      resolve('OK');
    else
      reject('NG');
    console.log(`executer called: ${num}`);
  });
}

function makeAsyncPromise(a1, b1) {
  return (async (a2, b2) => {
    let x = await makePromise(a2);
    let y = await makePromise(b2);
    return x + y;
  })(a1, b1);
}

makeAsyncPromise(2, 4).then((x) => {
  console.log(`then callback: ${x}`);
}).catch((r) => {
  console.log(`catch callback: ${r}`);
});
console.log('then and catch called');
結果
executer called: 2
then and catch called
executer called: 4
then callback: OKOK

例外

async内では、try - catchを使って例外またはrejectの結果を処理できます。

(async (a, b) => {
  try {
    let x = await makePromise(a);
    let y = await makePromise(b);
    console.log(`async function finished: ${x} ${y}`);
  }
  catch(e) {
    console.log(`error: ${e}`);
  }
})(2, 1);
結果
executer called: 2
executer called: 1
error: NG

async関数が返すPromiseに対してcatchを呼べば、try - catchを使う必要はなくなります。executerのrejectや、コールバック関数、async関数で発生した例外は、すべて最後のcatchで処理できます。

function makeAsyncPromise(a1, b1) {
  return (async (a2, b2) => {
    let x = await makePromise(a2);
    let y = await makePromise(b2);
    return x + y;
  })(a1, b1);
}

makeAsyncPromise(2, 1).then((x) => {
  console.log(`then callback: ${x}`);
}).catch((r) => {
  console.log(`catch callback: ${r}`);
});
console.log('then and catch called');
結果
executer called: 2
then and catch called
executer called: 1
catch callback: NG

すべてを同期するのは無理

ここまで長々と書いてきたのは、最近Puppeteerの使い方を調べたときに、async、awaitがよく分からなかったからです。今どきのJavaScriptライブラリではPromiseオブジェクトを返す関数が用意されてて、awaitを付けて使うものらしいですね。

次のような書き方で「1→2→3」の順に実行することはできないのか、というのが研究の目的でしたが、できないことが分かりました。

console.log("1");
(async (a, b) => {
  let x = await makePromise(a);
  let y = await makePromise(b);
  console.log("2");
})(2, 4);
console.log("3");

asyncとawaitを使っても、結局のところ非同期で動くPromiseが次々にできていくだけなので、最終的にはコールバック関数を使うしかありません。JavaScriptの1つの流れの中でPromiseすべてを順番に動かすのは無理です。

console.log("1");
(async (a, b) => {
  let x = await makePromise(a);
  let y = await makePromise(b);
  console.log("2");
})(2, 4).then((x) => {
  console.log("3");
});

以上です。気が向いたら追加するかもしれません。

参考文献

2
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2