この記事では、ES6のasync/await、およびPromises.all()を使用して非同期プログラミングを容易にする方法を学習します。
前回でのPromiseチュートリアルは下記で学べます。
async/awaitとは
async/awaitは技術的に言えば、Promisesのシンタックスシュガー(Syntax sugar)となります。
シンタックスシュガーとは簡単に言ってしまえば、構文を省略しプログラムを書いていく事をシンタックスシュガーと言います。
ですが、これはプログラミング言語によって異なってきますのでご注意下さい。
現代の非同期JavaScriptはかつてないほど簡単になりました。
JavaScriptは、コールバックからプロミス(ES2015)まで、すさまじい速さで進化しました。
また、ES2017以降、非同期JavaScriptは、async/await構文を使用することで同期コードのように見えるが、読みやすい非同期コードを記述でにるようになり、さらにシンプルになりました。
つまり、遥かむかしではコールバックを使用し、次にPromiseを使用し始め、現在はこれからあなたが学ぶasync/await構文となります。
よくある誤解は、async/awaitとPromiseは完全に異なるものであると思っている方です、決してそんな事はありません。
async/awaitはPromiseに基づいて構築されます。
これはasync/awaitはPromiseの拡張された構文でもあるという事になります。
async関数を使用すると、非同期JavaScriptを簡単に作成できますが、初心者にとっては困難でありまた独自の落とし穴があります。
最後に解説しますが、使用する際の注意点も抑えておいて下さい。
それでは、async/awaitの世界へ
asyncキーワード
まずはasyncキーワードから学んでいきましょう。
asyncは関数の前に配置させます。
これは構文で決まりになっていますので必ず、関数の前に記述して下さい。
通常の関数宣言で使用可能です。
async function functionName() { return 1; }
アロー関数の機能でも使用できます。
const functionName = async (x) => { }
つまりこれは、無名関数でも使用可能という意味にもなります。
これによってJavaScriptは、async/await構文を使用していることを認識できます。
asyncキーワードが配置された関数は常にPromiseを返すようになります。
つまり、あなたが何をしているのかは関係ありません戻り値は常にPromiseとなります。
const x = async () => { return 1 } const result = x() console.log(result) // Promise
では上記の関数を、1の結果で解決されたPromiseを返しアラートで通知させてみましょう。
const result = async () => 1; return().then(alert); // 1
値は、解決されたPromiseに自動的にラップされます。
See the Pen JSのPromiseのasync by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
これは次のような事とやっている事は同じになります。
const x = async () => { return Promise.resolve(1); } x().then(alert); // 1
明示的に書かなくとも、先述した通りに戻り値(return)は常にPromiseとなります。
したがって、async関数がPromiseを返し、Promise以外をラップするようにします。
難しい事はなく簡単ですね。
ですがそれだけではございません。
await関数内でのみ機能する別のキーワード、がありasyncは以前のPromiseと比べ、かなりクールな機能となっています。
awaitキーワード
このawaitキーワードにより、JavaScriptはそのPromiseが解決済みまたは拒否された結果を返すまで待機します。
const x = await myPromise;
await演算子は、Promiseを待機するために使用されますが非同期ブロック内でのみ使用可能となります。
同期関数内ではawaitは使用できません。
そして、awaitで呼び出される側の関数にはPromiseオブジェクトを(resolve)返してあげて下さい。
// 呼び出される側の関数 function myPromise() { return new Promise((resolve, reject) => { //何かしらの処理 resolve(0) //返す }) }
// awaitを使用する側 const sampleAsync = async () => { const result = await myPromise() } //functionキーワード async function sampleAsync() { const result = await myPromise() }
awaitが呼び出される側はPromiseオブジェクトを返す。
Promiseオブジェクト内では、returnで返さないようにして下さい。
awaitを使用する側の関数にはasyncを関数の前に配置する。
これら上記の注意点が初心者様は、うまく動作してくれずに思い通りのコードが書けないという状況になります、これは意外な落とし穴となっておりますのでご注意下さい。
それでは実際にawaitを使用してみましょう。
下記では、2秒後に解決するPromiseとなります。
まずはasyncを関数の前に配置させ、常にPromiseとして機能させます。
setTimeout()で2秒指定にします。
const x = async () => { let myPromise = new Promise((resolve, reject) => { setTimeout(() => resolve("解決済み!"), 2000) }); let result = await myPromise; console.log(result); } x();
awaitキーワードを使用していますね、これはPromiseが解決すると、待機していた処理を再開してresultの結果となっています。
let result = await myPromise;
ですので、上記のコードは2秒で『解決済み』を示しています。
つまりは、awaitの仕事はPromiseが解決するまで関数の実行を文字通り中断し、Promiseの結果で再開させます。
promise.then()よりも、さらに洗練されたPromiseの結果を取得するための構文となります。
See the Pen JSのPromiseのawaitのdemo by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
Async関数は常に待機する必要はありません
asyncで、awaitがなくても自由に使用しても構いません。
ですが、これはそれほどあまり一般的に広く使用されているわけではありませんが、Promiseで値をラップする必要がある際は非常に役に立ちます。
const sample = async () => { return "Hello" }
ですが前述したように、広く使用されていない為awaitの使用がない場合は、基本的にはasyncにこだわる必要はありません。
async関数では、複数のawaitキーワードを追加し待機が可能です。
例を見てみましょう。
const results = ms => { return new Promise(res => setTimeout(res, ms)) } const getResult1 = () => { return results(1000).then(x => 1) } const getResult2 = () => { return results(1000).then(x => 2) } const getResult3 = () => { return results(1000).then(x => 3) } const funAsync = async () => { const first = await getResult1() console.log(first) const second = await getResult2() console.log(second) const third = await getResult3() console.log(third) console.log('完了しました!') } funAsync()
上記ではsetTimeout()のミリ秒指定で1秒ずつthenによって順番に処理され、1・2・3で完了の通知をします。
See the Pen JSのasync/await 複数のawaitを待機 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
const funAsync = async () => { const first = await getResult1() console.log(first) const second = await getResult2() console.log(second) const third = await getResult3() console.log(third) console.log('完了しました!') }
上記のように、複数のawaitキーワードを追加して処理の待機ができます。
ですがこれは、必ずしも悪いということではありませんが、async/awaitは並列実行を行う方がはるかに高速な処理となっております。
なぜ複数の場合、並列を推奨されるのかを説明します。
const result = async () => { await promise1(10); // 10秒… await promise2(30); //さらに30秒待つ return "完了!"; }
先述で学んできたような処理と上記での、この場合では大きなリクエストや重い処理は非常に時間がかかる可能性があるため、これは最善の良い方法とは言えません。
ですので実行を並列させる必要があります。
Promise並列処理
実行を並列処理するには、Promise.all()関数を実行する方法です。
つまり複数の待機(await)を並行して同時に実行します。
async/awaitによる非同期処理を、いくつかまとめて並列実行したい場合に使用します。
このpromise.all()関数は、イテレータ内(配列)のすべてのPromiseが解決されたときに解決され、その結果を返します。
反復可能なPromiseを入力として受け取り、入力Promiseの結果の配列に解決される単一を返します。
複数のasync関数を呼び出すと、それらがすべて解決され、出力が配列に保存されるまで待機することができます。
簡単な例を見てみましょう。
const promiseTime1 = (value, delay) => { return new Promise((resolve) => setTimeout(() => resolve(value), delay)); } const promiseTime2 = (index, delay) => { return new Promise((reject) => setTimeout(() => reject(index), delay)); } const promiseResult = async () => { const promiseAll = Promise.all([ promiseTime1(["banana", "tomato"], 1000), promiseTime2(["orange", "apple"], 1000) ]); // wait...(待機) const arrayLists = await promiseAll; console.log(arrayLists); // 1秒後 // [[' banana', 'tomato'], ['orange', 'apple']] } promiseResult()
2つのPromiseすべてを配列に追加し、一連のPromiseを受け取り、新しいPromiseを返し、すべてのPromiseを同時に実行します。
上記では1秒後に同時実行されています。
const promiseResult = async () => { const promiseAll = Promise.all([ promiseTime1(["banana", "tomato"], 1000), promiseTime2(["orange", "apple"], 1000) ]);
下記の出力をアラート通知にしましたのでご確認下さい。
See the Pen JSのasync/await 並列処理 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
1つのPromiseが拒否された場合、メソッドは短絡してエラーをスローします。
これを正常に実行するには、すべてのPromiseを解決する必要があります。
その際はcatchを使用下さい。
const promiseResult = async () => { const promiseAll = Promise.all([ promiseTime1(["banana", "tomato"], 1000), promiseTime2(["orange", "apple"], 1000) ]) catch(err) { console.error(err) }
エラー処理については、後ほど詳しく解説致します。
async/awaitでのループ
ループでasync関数を使用すると、問題が発生する可能性があります。
もちろんですが回避策はございますが。
落とし穴がある事に注意して下さい、よくある初心者様はループが止まりません、機能しませんなどの方々が多くいます。
それはコールバック非同期関数が親のasync関数を一時停止できずに発生している事になります。
つまり、アロー関数を使用している場合はブロックとして機能している事を見落としている可能性があります。
ですが、async関数操作は多くの開発者をつまづかせてるのは間違いありません。
それのほとんどが、Arrayのループと組み合わせによるものです。
async配列要素に対して非同期操作を実行する場合は『 for…of 』または『for』がおそらく最も簡単な選択肢です。
『forEach』の場合はタスクが完了するのを待ちません。
つまりforEachは同期操作です。
コールバックを使用すると、残りのコードが実行され、非同期操作が待機されなくなります。
awaitは待機する事なく、次々とタスクを開始し次に進んで処理し始めます。
古典的なforでも良いですが、今回はfor…ofで例を見てみましょう。
const asyncDelay = () => { return new Promise(res => setTimeout(res, 100)) } const result = async (itemData) => { await asyncDelay(); console.log(itemData); } const testArray = async (array) => { for (const itemData of array) { await result(itemData); } console.log('完了!'); } testArray([1, 2, 3])
上記は正常な結果として取得が得られます。
配列の各要素それぞれの数値は順番に処理されてるのが分かるかと思います。
See the Pen JSのasync/await for…ofループ by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
Promise.all()を使用すれば、この処理を並列して実行する事も可能です。
const asyncDelay = () => { return new Promise(res => setTimeout(res, 100)) } const result = async (itemData) => { await asyncDelay(); console.log(itemData); } const testArray = async (array) => { const parallel = array.map(result); await Promise.all(parallel); console.log('完了!'); } testArray([1, 2, 3])
上記のPromise.all()はmap配列をPromise変換し作成しています。
全てのPromiseが解決されるのを待機し、並列して実行可能です。
See the Pen JSのasync/await 並行ループ by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
先述で学んだ、並列処理と混同しないように気をつけて下さい。
Promise.all()のasync配列要素は必ずしも並列処理を保証しているわけではない事を忘れないで下さい。
エラー処理
作成したプログラムでは常にエラーが発生する可能性があります。
特に現代のJavaScript非同期プログラミングでは、ファイルを読み込もうとすると、ファイルが利用できない場合があります。
ですのでasync/awaitでのエラー処理は、多くの混乱を引き起こします。
async関数のエラーを処理するためのパターンは多数あり、経験豊富な開発者でさえ、間違えることがあります。
まずは、最も一般的で簡単な方法を紹介します。
async関数からスローされたエラーは、拒否(reject)されたPromiseとなります。
Promiseが正常に解決された場合await promiseの結果を返します。
ただし、拒否された場合は、throwでその行にawait promiseがあるかのようにエラーがスローされます。
const x = async () => { await Promise.reject(new Error("Warning!")); }
同等になります。
const x = async () => { throw new Error("Warning!"); }
throw errorエラーの場合では、その場所で呼び出された場合と同じように、例外が生成されます。
それ以外の場合は、結果を返します。
エラーの処理の際はPromiseが拒否されるまでに、時間がかかる場合があります。
予期しない例外によるプログラムのクラッシュを防ぐために、JavaScriptフロー制御ではエラー処理が非常に重要です。
Promiseでエラーが発生した場合は、async/await構文の最も優れた機能の1つである、同期コードを記述しているのと同じように、標準の『try-catch』が使用可能であることです。
try / catch
const test = async () => { try { const foo = await getFoo(false); } catch (error) { alert(error) // 失敗! } } test()
See the Pen JSのPromiseのasync/await エラー処理 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
素晴らしい機能ですが、async関数内に複数のawaitキーワードがある場合、エラー処理が見にくい可能性があります。
例えば下記のように。
const test = async () => { try { const foo = await getFoo(false); } catch (error) { alert(error) // 失敗! } } try { const foo = await getFoo(false); } catch (error) { alert(error) // 失敗! } } try { const foo = await getFoo(false); } catch (error) { alert(error) // 失敗! } } test()
上記が複雑な処理だった場合、適切にエラー処理するのが大変です。
より良い回避方法がございます。
async関数は常にPromiseを返すことを私たちは学びましたね。
Promiseを呼び出すと、catch呼び出しのエラーを処理できます。
これは、catchを追加することでasync関数からのエラーを適切に処理できることを意味しています。
const test = async () => { const result1 = await getResult1(false); const result2 = await getResult2(false); const result3 = await getResult3(false); } test().catch(error); //または test().catch(error => alert(error))
See the Pen JSのasync/await エラー処理 2 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
catchを追加するのを忘れると、未処理のPromiseエラーが発生します。
注意点は、demoを確認してもらうと分かると思いますが、Promiseのcatchメソッドでは、1つのエラーのみをキャッチできるという点です。
async function innerAsyncFn(){ return Promise.reject(Error("Error from Asynchronous Fn")); }
複数のawaitを待機する場合は、Promiseが解決する前に遅延処理で関数を作成して下さい。
promise.allメソッドなどの活用です。
複数のPromiseを同時にフェッチもできます。
ほとんどは、すべてをtry / catchでラップするだけで、快適で安全になります。
.catch()はasync関数を連鎖(チェーン)させることができる高階関数となります。
エラー処理の方法は様々ございますので、また別途まとめて記事に致します。
async/awaitでFetchを使用する方法
Fetch APIは、純粋なネイティブJavaScriptに付属する非同期Web APIであり、Promiseの形式でデータを返します。
Fetch APIを使用すると、他のAPIと通信が可能となります。
これは、Promiseを使用して『HTTP/1.1プロトコル』を介してネットワーク要求を行うWeb APIとなります。
Fetch APIを使用して、同じリクエストまたはクロスオリジンリクエストの両方を行うができます。
簡単な例をご用意しました。
単純なasyncのGETリクエストを生成しfetchします、ダミー用で提供されている、ユーザーデータを応答オブジェクトからJSON形式で返します。
それではデータベースからユーザーを取得しましょう。
const testRequest = async () => { let url = 'https://jsonplaceholder.typicode.com/users'; let res = await fetch(url); if (res.ok) { let json = await res.json(); alert("成功! データ取得メッセージです"); return json; } else { return `HTTP error: ${res.status}`; } } testRequest().then(data => { console.log(data); });
上記はコールバックを使用しています。
.thenメソッドでPromiseオブジェクトに渡されたコールバック関数の処理結果を取得し、結果を返します。
if(res.ok){}
このokプロパティは、HTTPステータスコードの200-299に対してブール値でtrueを返します。
See the Pen JSのasync/await GETリクエスト by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
取得の失敗を出力したい場合は、res.okを論理否定演算子でブール値を反転させて下さい、trueからfalseに反転します。
if( !res.ok)
try/catchでエラー処理する場合は下記のようにします。
const getUsers = async (users) => { try { const response = await fetch(`https://jsonplaceholder.typicode.com/users${users}`); const data = await response.json() if (!data.ok) throw new Error('ユーザーデータの取得に問題があります。'); } catch (error) { console.error(`\u{26A0}Warning: ${error.message}`) } } getUsers('non-user'); 出力: ⚠Warning: ユーザーデータの取得に問題があります。
See the Pen JSのasync/await try/catchでの処理 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
ブロックでエラーが発生した場合、tryブロック内のコードcatchが実行されます。
エラーがコンソールに到達して、スクリプトの実行を停止する前に、エラーをキャッチして処理が可能です。
※先述でも言いましたが実際では、Promiseがエラーをスローするまでにしばらく時間がかかるので焦らないで下さい。
async関数では、必ず常に何かしらの約束を返すようにします。
使用に関する注意点(ルール)
・awaitキーワードは通常の関数内で使用することはできません。
const firstAsync = () => { let promise = Promise.resolve(1); let result = await promise; // Syntax error }
function x() { let myPromise = Promise.resolve(1); let result = await myPromise; // Syntax error }
正しく機能させるには、かならず関数の前にasyncを追加する必要がありますので気をつけて下さい。
関数の前にasyncキーワード置くのを忘れると、Syntax errorが発生する可能性があります。
・asyncキーワードの後に使用する関数は、awaitである場合とそうでない場合があります。
非同期関数でなければならないという必須の規則はございません。
・awaitがサポートされていない環境でトップレベルを使用するには、無名関数にトップレベルをラップすることです。
(async () => { const user = await fetch(""); })();
即時関数としてラップしてください。
まとめ
・asyncキーワードは関数の前に必ず配置させます。
・awaitは、Promiseを待機するために使用されますがasyncブロック内でのみ使用可能となっています。
・async関数は常にpromiseを返します、戻り値は必ずpromiseとなっています。
・awaitが必要ない場合は、無理にasync関数を使用する必要はございません。
・promiseが解決されるまで、awaitは解決される前に遅延を作成できます。
・複数のawaitを待機させる場合は、Promise.all()を使用すれば並列として同時に実行可能です。
・awaitをループ処理する際は、『for…of』または『for』を使用するのが良い選択肢です、またPromise.all()を活用すれば並列として実行できますが、通常の並列と混同しないように注意が必要です。
・エラー処理の際は、try/catchで呼び出しを使用する必要があります、ですがcatchメソッドは1つのエラーのみをキャッチします。
最後に
Async Awaitは非常に強力となっておりますが、注意も必要です。
しかし、それらを適切に使用すると、コードを非常に読みやすく効率的にするのに役立ちます。
Promise、Observablesへのサブスクリプション、およびsetTimeout()呼び出しはすべて、実行する前にメソッドが完了するのを待つ必要があります。
Promiseベースのasync関数での非同期呼び出しは、あらゆるコールバックパターンを改善しエラーが発生しにくい状態に保ちながら、関数を順番に呼び出し処理することができます。
直感的に理解を望んでる場合は、コードをひたすら書き続け実験かのように繰り返すのが最善の方法です。
動作がおかしいと気付いた際は、なぜそれが発生してしまったのかを理解するように心掛けてください。
JavaScript PromiseのAsync/Awaitについて、精通してる場合の方は、当記事は単純な処理で浅すぎると思われてる方もいらっしゃるとおもいます。
このチュートリアルは、Async/Awaitがどのように機能するかについてより理解を深めてもらい、目標に向けた一歩を踏み出してもらうための、初心者様のプログラマーまたはエンジニアを対象としています。
これで基本的な基礎である、async関数について知っておく必要がある事はありません。
さらに1ステップ進むための、様々なエラー処理方法やJavaScriptでasyncにリソースをfetchする方法も沢山ありますので、それらも今後記事にしていきと思います。
本日は以上となります。
最後までこの記事を読んで頂きありがとうございます。