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/catch
async/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…)を使用してください。