ES2017で仕様に入ったAsyncFunction
とawait
単項演算子。
これらを使うと非同期処理を同期的に書くことができ、非同期処理のループもシンプルに書けるようになる。
この記事の内容は全て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
は使えないらしい。