JavaScript
Node.js

JavaScriptのasync/awaitがPromiseよりもっと良い

More than 1 year has passed since last update.

Node.js V7.6からasync/awaitが正式に使えるようになりました。

async/awaitの説明

  • async/awaitが非同期処理を実装する新しいやり方である。
     今までがコールバック及びPromiseを使っていた。

  • Promiseをベースにしているが、普通のコールバックまたはNodeコールバックを共用できない。

  • async/awaitの非同期処理がアンブロッキングである。

  • 非同期処理が同期処理のような見た目になり、ソースコードの見通しが良くなる。

文法例

下記の例で説明します。
getJSON関数で非同期通信でネットAPIを叩いてPromiseオブジェクトを返します。レスポンスデータをコンロールログに取得します。

Promiseを利用

const makeRequest = () =>  
  getJSON().then(data => {  
    console.log(data);
    return "done";  
  });  
makeRequest()

async/awaitを利用

const makeRequest = async () => {  
    console.log(await getJSON())  
    return "done"  
}  
makeRequest()
  • awaitがasyncで定義された関数内にしか使えません。async関数が暗黙的にPromiseを返し、関数の戻り値がPromiseの処理完了の値になります。
  • asyncの中に入らないので、ソースコードのトップレベルでawaitを使ってはいけません。
  • await getJSON()の実行で止まってしまい、処理が終わるまでにconsole.log処理が待ち状態になります。

async/awaitがもっと良い理由

1. ソースコードが簡潔

thenの匿名関数の記載が不要になります。その効果の累積で複雑な処理にはメリットが更に大きくなります。

2. エラー処理が簡単

次の例では、Promiseを利用する場合、同期処理のtry catchと非同期処理のtry catchをそれぞれ記載しなければなりませんが、async/awaitを利用すれば、一箇所でtry catchすれば良い。

Promise

const makeRequest = () => {  
  try {
    // 同期処理Aでも例外が発生する可能性がある
    functionA();
 
    getJSON().then(result => {  
      // JSONパース例外が発生
      const data = JSON.parse(result);
      console.log(data);
    })
    // getJSONの非同期処理で発生した例外をキャッチ
    .catch((err) => {  
      console.log(err);
    });
 
    // 同期処理Bでも例外が発生する可能性がある
    functionB();
 
  // 同期処理A、同期処理B、getJSON関数内に非同期処理の前の同期処理で発生した例外をキャッチ
  } catch (err) {
    // ここでJSONパース例外をキャッチできない
    console.log(err);
  }
}

async/await

const makeRequest = async () => { 
  try {
    // 同期処理Aでも例外が発生する可能性がある
    functionA();
    // JSONパース例外が発生
    const data = JSON.parse(await getJSON());
    console.log(data);
    // 同期処理Bでも例外が発生する可能性がある
    functionB();
  } 
  catch (err) {
    // ここで同期処理Aと同期処理B以外、JSONパース例外もキャッチできる
    console.log(err)
  }
}

3. 処理分岐の見通しが良い

下記は1回目の通信結果によって、2回目の通信を行うかどうかを決める場合の例です。
Promiseを利用する場合のネスト、括弧および戻り値により見通しが悪いに対して、async/awaitの利用で見通しが明らかに良くなります。

Promise

const makeRequest = () => {  
  return getJSON().then(data => {  
    if (data.needsAnotherRequest) {  
      return makeAnotherRequest(data).then(moreData => {  
        console.log(moreData); 
        return moreData;
      });
    } else {  
      console.log(data);
      return data;
    }  
  });
}

async/await

const makeRequest = async () => {  
  const data = await getJSON();
  if (data.needsAnotherRequest) {  
    const moreData = await makeAnotherRequest(data);  
    console.log(moreData);
    return moreData;
  } else {  
    console.log(data);
    return data;
  }
}

4. 複数の途中結果を使う場合がわかりやすくなります

下記は、Promise1の戻り値によってPromise2を実行し、Promise1とPromise2の戻り値でPromise3を実行する例です。

Promise

const makeRequest = () => {  
  return promise1()  
    .then(value1 => {  
        // do something  
        return promise2(value1)  
            .then(value2 => {  
            // do something
            return promise3(value1, value2);
        })  
    });
}

async/await

const makeRequest = async () => {  
  const value1 = await promise1();
  const value2 = await promise2(value1);
  return promise3(value1, value2);
}

5. エラー発生時のスタックトレース情報が詳しい

下記は非同期処理がチェーンで呼び出される場合に発生したエラー場所を判明し難いに対して、async/awaitを利用する場合、エラー発生の関数名が確実にわかります。

Promise

const makeRequest = () => {  
  return callAPromise()  
    .then(() => callAPromise())  
    .then(() => callAPromise())  
    .then(() => callAPromise())  
    .then(() => callAPromise())  
    .then(() => {  
      throw new Error("oops");  
    });
};
makeRequest()  
.catch(err => {  
  console.log(err);  
  // 出力にcallAPromiseの呼び出ししかわかりません
  // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)  
});

async/await

const makeRequest = async () => {  
  await callAPromise();
  await callAPromise(); 
  await callAPromise(); 
  await callAPromise(); 
  await callAPromise(); 
  throw new Error("oops");  
};
makeRequest()  
.catch(err => {  
  console.log(err);  
  // 出力には関数名がわかります 
  // Error: oops at makeRequest (index.js:7:9)  
});

6. async/awaitがデバッグしやすい

  • Promiseのデバッグ制限

    1. 関数本体が戻り値しかないアロー関数をデバッグできない。
    2. 同期ソースコードに対してステップインでデバッグしかできないので、thenブロックから次のthenブロックにステップインできない。
  • async/awaitでは非同期処理を同期処理みたいにステップインでデバッグできる。

まとめ

async/awaitを使えば非同期処理を直感的にかつ簡潔な書き方で実装できます。
それが今までの非同期処理の混乱を解決する最終案ではないかと思います。
新しいプロジェクトが立ち上がる時にできる限り非同期処理をasync/awaitで実装するのがお薦めです。

参考記事

Node.js 8.0リリース
async funciton(MDN)