Promiseをより快適に扱うための特別な構文に「async/await」があります。驚くほど理解しやすく、使いやすいです。
非同期関数
asyncキーワードから始めましょう。関数の前にこのように置くことができます。
async function f() {
return 1;
}
関数の前に「async」という単語を付けるということは、1つの簡単なことを意味します。関数は常にPromiseを返します。他の値は解決済みのPromiseに自動的にラップされます。
例えば、この関数は結果が1である解決済みのPromiseを返します。テストしてみましょう。
async function f() {
return 1;
}
f().then(alert); // 1
…明示的にPromiseを返すこともできますが、それは同じです。
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
そのため、asyncは関数がPromiseを返すことを保証し、Promise以外のものをラップします。簡単ですね?しかしそれだけではありません。もう1つのキーワードawaitがあり、これはasync関数内でのみ機能し、非常にクールです。
Await
構文
// works only inside async functions
let value = await promise;
キーワードawaitは、そのPromiseが解決するまでJavaScriptを待たせ、その結果を返します。
1秒後に解決するPromiseの例を次に示します。
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
関数の実行は(*)行で「一時停止」し、Promiseが解決されると再開され、resultはその結果になります。そのため、上記のコードは1秒後に「完了!」と表示されます。
強調しておきましょう。awaitは、Promiseが解決するまで関数の実行を文字通り中断し、Promiseの結果で再開します。これはCPUリソースを消費しません。なぜなら、JavaScriptエンジンはそれまでの間に他のタスクを実行できるからです。他のスクリプトを実行したり、イベントを処理したりなどです。
これは、promise.thenよりもPromiseの結果を取得するためのよりエレガントな構文です。そして、読み書きが簡単です。
awaitを使用できません非同期関数以外でawaitを使用しようとすると、構文エラーが発生します。
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
関数の前にasyncを付けるのを忘れると、このエラーが発生する可能性があります。前述のように、awaitはasync関数内でのみ機能します。
第Promiseチェイニング章のshowAvatar()の例を取り上げ、async/awaitを使用して書き直してみましょう。
.then呼び出しをawaitに置き換える必要があります。- また、それらが機能するために、関数を
asyncにする必要があります。
async function showAvatar() {
// read our JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// read github user
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// show the avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
かなりクリーンで読みやすいですね?以前よりはるかに優れています。
awaitが許可されています最新のブラウザでは、モジュール内であれば、最上位レベルのawaitは問題なく機能します。モジュールについては、記事モジュール入門で説明します。
例えば
// we assume this code runs at top level, inside a module
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
console.log(user);
モジュールを使用していない場合、または古いブラウザをサポートする必要がある場合は、普遍的な方法があります。匿名の非同期関数にラップします。
このように
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
awaitは「thenable」を受け入れますpromise.thenと同様に、awaitを使用すると、thenableオブジェクト(呼び出し可能なthenメソッドを持つオブジェクト)を使用できます。これは、サードパーティのオブジェクトがPromiseではない可能性がありますが、Promiseと互換性があります。.thenをサポートしていれば、awaitで使用するには十分です。
ここにデモThenableクラスがあります。下のawaitはそのインスタンスを受け入れます。
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// resolve with this.num*2 after 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// waits for 1 second, then result becomes 2
let result = await new Thenable(1);
alert(result);
}
f();
awaitが.thenを持つ非Promiseオブジェクトを取得した場合、そのメソッドを呼び出し、組み込み関数resolveとrejectを引数として提供します(通常のPromise実行関数と同様です)。その後、awaitはそれらのいずれかが呼び出されるまで待ちます(上記の例では(*)行で発生します)。そして、結果で続行します。
非同期クラスメソッドを宣言するには、asyncを前に付けるだけです。
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1 (this is the same as (result => alert(result)))
意味は同じです。返された値がPromiseであることを保証し、awaitを有効にします。
エラー処理
Promiseが正常に解決された場合、await promiseは結果を返します。しかし、拒否された場合、その行にthrowステートメントがあるかのように、エラーをスローします。
このコード
async function f() {
await Promise.reject(new Error("Whoops!"));
}
…はこれと同じです。
async function f() {
throw new Error("Whoops!");
}
実際には、Promiseが拒否されるまでに時間がかかる場合があります。その場合、awaitがエラーをスローするまで遅延が発生します。
通常のthrowと同様に、try..catchを使用してそのエラーをキャッチできます。
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
エラーが発生した場合、制御はcatchブロックにジャンプします。複数の行をラップすることもできます。
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
try..catchがない場合、非同期関数f()の呼び出しによって生成されたPromiseは拒否されます。.catchを付加して処理できます。
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)
.catchを追加し忘れると、未処理のPromiseエラーが発生します(コンソールで確認できます)。第Promiseによるエラー処理章で説明されているように、グローバルなunhandledrejectionイベントハンドラを使用して、そのようなエラーをキャッチできます。
async/awaitとpromise.then/catchasync/awaitを使用する場合、awaitが待機を処理するため、.thenはほとんど必要ありません。そして、.catchの代わりに通常のtry..catchを使用できます。これは通常(常にではありませんが)、より便利です。
しかし、コードの最上位レベル、つまり非同期関数の外側では、構文上awaitを使用できません。そのため、上記の例の(*)行のように、最終的な結果またはフォールスルーエラーを処理するために.then/catchを追加するのが一般的な方法です。
async/awaitはPromise.allと連携して機能します複数のPromiseを待機する必要がある場合、それらをPromise.allでラップしてからawaitできます。
// wait for the array of results
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
エラーが発生した場合、通常どおり、失敗したPromiseからPromise.allに伝播し、その後、呼び出しの周りにtry..catchを使用してキャッチできる例外になります。
まとめ
関数の前にasyncキーワードを付けることで、2つの効果があります。
- 常にPromiseを返すようにします。
awaitの使用を許可します。
Promiseの前にawaitキーワードを付けることで、JavaScriptはそのPromiseが解決するまで待ち、その後
- エラーの場合、例外が生成されます。その場所で
throw errorが呼び出された場合と同じです。 - そうでない場合は、結果を返します。
これらは組み合わさり、読み書きが容易な非同期コードを作成するための優れたフレームワークを提供します。
async/awaitを使用すると、promise.then/catchを書くことはめったにありませんが、それらがPromiseに基づいていることを忘れてはいけません。なぜなら、場合によっては(例えば、最外部スコープでは)これらのメソッドを使用する必要があるからです。また、多くのタスクを同時に待機する場合、Promise.allは便利です。
コメント
<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合は、サンドボックス(plnkr、jsbin、codepen…)を使用してください。