30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

async/awaitを使った非同期処理の書き方

ES2017で仕様に入ったAsyncFunctionawait単項演算子。
これらを使うと非同期処理を同期的に書くことができ、非同期処理のループもシンプルに書けるようになる。

この記事の内容は全てNode.jsのv8.6.0で動作確認している。

非同期処理の基礎はこちら。

AsyncFunction

関数定義の前にasyncとつけると、その関数はAsyncFunctionになる。

async function myFunc(){
  return 'foo';
}

console.log(myFunc); // [AsyncFunction: myFunc]
console.log(myFunc()); // Promise { 'foo' }
myFunc().then(res => console.log(res)); // foo

非同期処理をシンプルに書けていることが分かる。
AsyncFunctionは、returnしたものをPromise.resolve()でラップして返す。
だから、returnしなければラップされたundefinedを返す。

async function myFunc(){}
console.log(myFunc()); // Promise { 'undefined' }
myFunc().then(res => console.log(res)); // undefined

promiseオブジェクトの場合も同じで、Promise.resolve()でラップされたpromiseオブジェクトを返す。

async function myFunc(){
  return new Promise(resolve => {
    setTimeout(function() { resolve('boo') }, 0)
  });
}
console.log(myFunc()); // Promise { <pending> }
myFunc().then(res => console.log(res)); // boo
async function myFunc(){
  return new Promise((resolve, reject) => {
    setTimeout(function() { reject('boo') }, 0)
  });
}
console.log(myFunc()); // Promise { <pending> }
myFunc().catch(res => console.log(res)); // boo

AsyncFunctionのなかで何かをスローすると、スローされた値をPromise.reject()でラップしたものを返す。

async function myFunc(){
  throw 'fail';
}
console.log(myFunc()); // Promise { <rejected> 'fail' }
myFunc()
  .then(res => console.log(res))
  .catch(res => console.log(`reject. ${res}`)); // reject. fail

await

awaitは単項演算子で、AsyncFunctionのなかでのみ使える。それ以外の場所で使おうとするとシンタックスエラーになる。

promiseオブジェクトの前にawaitを書くことで、そのオブジェクトがPendeingから状態変化してFulfilledもしくはRejectedになるまで処理を待つ。

そしてFulfilledになった場合、そのpromiseオブジェクトが持っている値を、awaitは返す。
Rejectedのケースでは、後述するように例外を投げる。

function returnFoo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('foo'), 0);
  })
}

async function notAwait(){
  const resutl = returnFoo();
  console.log('notAwait ->', resutl);
}

async function useAwait(){
  const resutl = await returnFoo();
  console.log('useAwait ->', resutl);
}

notAwait(); // notAwait -> Promise { <pending> }
useAwait(); // useAwait -> foo

awaitが返すのはpromiseオブジェクトではなくそれが持っている値である。そのため、then()などを使うことは出来ない。

function returnFoo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('foo'), 0);
  })
}
async function myFunc(){
  console.log(returnFoo()); // Promise { <pending> }
  console.log('then' in returnFoo()); // true
  console.log(await returnFoo()); // foo
  console.log(typeof await returnFoo()); // string
}

myFunc(); 

awaitが非同期処理の完了を待ってくれるので、まるで同期処理のように書くことが出来る。

function hello() {
  return new Promise(resolve => {
    setTimeout(() => resolve('Hello '), 0);
  })
}

function world() {
  return new Promise(resolve => {
    setTimeout(() => resolve('world '), 0);
  })
}

function exclamation() {
  return new Promise(resolve => {
    setTimeout(() => resolve('!'), 0);
  })
}

function notAwait() {
  const h = hello();
  const w = world();
  const e = exclamation();
  console.log(h, w, e); // Promise { <pending> } Promise { <pending> } Promise { <pending> }
  setTimeout(() => console.log(h, w, e), 0); // Promise { 'Hello ' } Promise { 'world ' } Promise { '!' }
}

async function useAwait() {
  const h = await hello();
  const w = await world();
  const e = await exclamation();
  console.log(h, w, e); // Hello  world  !
}

notAwait();
useAwait();

例外処理

上で少し触れたが、promiseオブジェクトがRejectedになった場合、awaitは例外を投げる。
promiseオブジェクトが持っている値を投げる。

function returnFoo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject('fail'), 0);
  })
}
async function useAwait(){
  try {
    const resutl = await returnFoo();
  } catch(e) {
    console.error(e); // fail
    console.error(typeof e); // string
  }
}
useAwait();

returnFoo()が返すのはRejectedなpromiseオブジェクトなので、直接catchをつなげて対応することも出来る。

function returnFoo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject('fail'), 0);
  })
}
async function useAwait(){
  const result = await returnFoo()
    .catch(res => {
      console.log('catch, ', res) // catch,  fail
      return res; // Promise.resolve(res) を返しそれを await が受け取るので、result は fail になる
    });
  console.log(result); // fail
}

useAwait();

では、try-catchでも.catch()でも例外をハンドリングしなかった場合、どうなるか。
冒頭で既に書いている。

AsyncFunctionのなかで何かをスローすると、スローされた値をPromise.reject()でラップしたものを返す。

そのため、次のように書ける。

function returnFoo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject('fail'), 0);
  })
}
async function useAwait(){
  const result = await returnFoo();
  console.log('This do not show.');
}

useAwait().catch(res => console.log('catch, ', res)); // catch,  fail

Promise.all

Promise.all()でも、awaitは問題なく機能する。

function hello() {
  return new Promise(resolve => {
    setTimeout(() => resolve('Hello '), 0);
  })
}

function world() {
  return new Promise(resolve => {
    setTimeout(() => resolve('world '), 0);
  })
}

function exclamation() {
  return new Promise(resolve => {
    setTimeout(() => resolve('!'), 0);
  })
}

(async () => {
  const result = await Promise.all([hello(), world(), exclamation()]);
  console.log(result); // [ 'Hello ', 'world ', '!' ]
})();

非同期処理のループ

async/awaitによって、非同期処理のループもシンプルに書けるようになる。

複数の非同期処理を直列で処理したいとき、役に立つ。
非同期処理の数が少なかったり、処理の内容が固定の場合は、then()でつなげていけばいい。
だが数が多かったり、処理の内容が固定でない場合は、厳しい。

async/awaitなら次のように書ける。

function hello() {
  return new Promise(resolve => {
    setTimeout(() => resolve('Hello '), 0);
  })
}

function world() {
  return new Promise(resolve => {
    setTimeout(() => resolve('world '), 0);
  })
}

function exclamation() {
  return new Promise(resolve => {
    setTimeout(() => resolve('!'), 0);
  })
}

(async () => {
  let msg = '';
  const asycFunctionList = [hello, world, exclamation];
  for (let func of asycFunctionList) {
    msg = msg + await func();
  }
  console.log(msg); // Hello world !
})();

非同期処理の結果を次の処理に渡していく場合は、こうなる。

function addHello(string) {
  return new Promise(resolve => {
    setTimeout(() => resolve(string + 'Hello '), 0);
  })
}

function addWorld(string) {
  return new Promise(resolve => {
    setTimeout(() => resolve(string + 'world '), 0);
  })
}

function addExclamation(string) {
  return new Promise(resolve => {
    setTimeout(() => resolve(string + '!'), 0);
  })
}

(async () => {
  let msg = '';
  const asycFunctionList = [addHello, addWorld, addExclamation];
  for (let func of asycFunctionList) {
    msg = await func(msg);
  }
  console.log(msg); // Hello world !
})();

最初はfor-ofではなくasycFunctionList.forEachでやろうとしたが、ダメだった。
このStack Overflowによると、このケースだとforEachは使えないらしい。

参考資料