awaitって何?
awaitはプロミスの履行を待つ構文です。
これを使うと、Promiseのthenをよりすっきりとしたコードに置き換えることができます。
awaitを使わずにプロミスの結果を使いたいとき、通常はthenメソッドを使います。
順番に実行した非同期処理が複数あるなら、thenをチェインしていました。
fetch('https://example.com') // HTTPリクエストを送信
.then(res => res.text()) // レスポンスボディを取得
.then(text => console.log(text)) // 出力
awaitを使うと、このコードを以下のように書くことができます。
// Responseオブジェクトが入る
const response = await fetch('https://example.com')
// レスポンスボディを取得する
const body = await response.text()
console.log(body) // 出力する
詳細
awaitは式の左に置くことができます。
また、awaitの右にPromiseオブジェクトを置くことで、そのプロミスの履行結果を取得できます。
const promise = // Promiseオブジェクト
const result = await promise
結果の取得に時間がかかる場合、await以降の処理はその時間が経ってプロミスが履行されてから実行されます。
そのためawaitを書いた部分の実行には時間がかかることがあります。
なお、このようにプロミスを直接置くより、プロミスを返す関数を書くことが多いです。
const result = await プロミスを返す関数()
asyncって何?
asyncは関数の前につけることができるキーワードです。
functionの前につけたり、アロー関数の前につけることもできます。
// 関数宣言の前にasyncをつける
async function name() {
// 処理
}
そして、awaitはasyncがついた関数の中でしか使えません。
例えば、次のコードは構文エラーとなります。
// asyncがない関数
function fetchExample() {
return await fetch('example.com')
}
このエラーを修正するには、関数の前にasyncをつける必要があります。
// 先頭にasyncをつける
async function fetchExample() {
return await fetch('example.com')
}
コードのトップレベルならTop-Level Awaitという仕様があるので、例外的に使えます。
戻り値
asyncがついている関数は戻り値がプロミスになります。
async function example() {
console.log('Hello World!')
}
example() // Promise
// Hello World! <- console.logは実行される
このプロミスは関数内の処理が終わったとき(最後のawaitが履行されたとき)に履行されます。
async function fetchExample() {
const res = await fetch('example.com')
const text = await res.text() // textはstringになる
return text
}
fetchExample() // Promiseが返ってくる
await fetchExample() // string
もしasyncをつけた関数内に一つもawaitがない、かつ関数がプロミスを返さない場合、このプロミスは即座に履行されます。
わざわざasyncをつける理由として、この挙動が挙げられると思います。
もしasyncがなかったとして、awaitをつけた処理があるだけで関数の戻り値が勝手にプロミスになるのは問題です。
そのため明示的にasyncをつけ、この中では非同期処理が実行されているよ、この関数はプロミスを返すよ、というのが一目でわかるようにします。
おまけ
エラーハンドリング
プロミスでの処理内で発生したエラーは、通常のエラーとは違ってtry-catchで捕捉できません。
その代わりプロミスのcatchメソッドで捕捉できます。
function getPromise() {
// 処理内でエラーが発生するプロミス
return new Promise(() => {
throw new Error('Error!')
})
}
// try-catchの場合
try {
getPromise()
} catch (e) {
console.log('エラーをキャッチ')
}
// Uncaught (in promise) Error: Error!
// エラーをキャッチできていない
// catchメソッドの場合
getPromise().catch(e => console.log('エラーをキャッチ'))
// エラーをキャッチ
awaitの場合
しかし、関数の前にawaitをつけると話は変わってきます。
awaitをつけたプロミス内で発生したエラーは、なんと通常のtry-catchで捕捉できます。
try {
await getPromise() // awaitをつける
} catch (e) {
console.log('エラーをキャッチ')
}
// エラーをキャッチ
catchメソッドとawait
関数の呼び出しの後にcatchメソッドを繋げ、それをawaitしてもエラーは捕捉できます。
try {
// awaitをつける & catchメソッドでハンドリング
await getPromise()
.catch(e => console.log('エラーをキャッチ in catchメソッド'))
} catch (e) {
console.log('エラーをキャッチ')
}
// エラーをキャッチ in catchメソッド
// try-catchのcatch句内での処理は実行されない
// 普通にこれだけでもOK
await getPromise()
.catch(e => console.log('エラーをキャッチ in catchメソッド'))
// エラーをキャッチ in catchメソッド
しかし、処理の流れがどうなるのかが直感的ではないので、特別な理由がない限りはやるべきではないと思います。
何が推奨?
非同期処理内でのエラーを補足するとき、推奨度合いは大まかに以下の表のようになると思います。
| 呼び出し方法 | エラーの補足方法 | 推奨度合い |
|---|---|---|
awaitでの呼び出し |
try-catch |
推奨 |
| 通常の呼び出し |
catchメソッド |
OK(NG派もいる) |
awaitでの呼び出し |
catchメソッド |
わかりづらいので非推奨 |
| 通常の呼び出し | try-catch |
絶対にNG: そもそも補足できない |
とは言ったものの、最後を除いては人によると思います。
プロミスを返す関数にasyncをつけたら?
asyncをつけなくともプロミスを返す関数は定義できます。
例えばPromiseのコンストラクタからプロミスを作成した場合です。
function getPromise() {
return new Promise((resolve, reject) => {
// 非同期で実行したい処理
})
}
現在のgetPromise関数の戻り値は、コード内のnew Promiseで作ったプロミスと同じです。
では、この関数の先頭にasyncをつけたらどうなるでしょうか。
asyncをつけた関数は、コード内でnewされたプロミスとは別の新しいプロミスを返します。
これはコード内のプロミスをラップしたものなので、実際に呼び出したときの動作は変わりません。
async function getPromiseAsync() {
// このプロミスをラップしたプロミスが戻り値になる
return new Promise((resolve, reject) => {
// 非同期で実行したい処理
resolve()
})
}
// 関数の動作は変わらない
await getPromise()
await getPromiseAsync()
詳細はMDNをご覧ください。