はじめに
promiseを使うとき、いつもpromiseメソッドチェーンで記載していますか? async/awaitを利用していますか?
もちろん状況によって両方書くのが殆どだとは思うのですが、私はasync/awaitの方が同期的な書き方ゆえに読みやすいため、なるべくそちらで記載しています。しかしながら、エラーハンドリングが理解できていなかったため、エラーの所在を突き止めるのに苦労してしまいました。
そのため、これを機にasync/awaitにおけるエラーハンドリングについて備忘録的にまとめておきます。
この記事のまとめ;
- catchされるエラーはrejectのみか、throwされたエラーも含まれるか
→両方catchできる - async関数における処理の順序、awaitがある場合とない場合
→awaitがない場合には同期的に処理が実行され、catchできなくなる - エラー処理を外側に伝播していく方法
→asyncのtry/catchで繋げられる
catchされるエラーはrejectのみか、throwされたエラーも含まれるか
そもそも、catchされなかったrejectはどうなる?
const f = () => new Promise(function(resolve, reject) {
reject(new Error("rejected Error!"));
}
)
f() //Uncaught (in promise) Error: rejected Error!
Uncaught (in promise) Errorが発生する。
このプロミス内で発生するエラーは以下のようにcatchハンドラを記載することでエラー時の処理を定義できる。
const f = () => new Promise(function(resolve, reject) {
reject(new Error("rejected Error キャッチ!"));
}
}).catch((e) => {console.log('catch!'})
f() //catch!
なお、f()自体はfullfilledされたPromiseを返す。そのため、この後にthenハンドラを追記して処理を非同期に続けていくこともできる。
'unhandledrejection'Eventをリッスンしてみる
ここで'unhandledrejection'イベントをListenすることでもどのようにエラーが起きたのかをみることができる。
以下ではrejectではなく単純にErrorを投げてみて、それが'unhandledrejection'イベントでキャッチできるかを調べてみる。
window.addEventListener('unhandledrejection', function(event) {
console.log('エラー元のpromise:', event.promise); // エラー元のpromise: Promise {<rejected>: Error: thrown Error もrejectedされるよ
console.log('エラー:',event.reason); // エラー: Error: thrown Error もrejectedされるよ
});
const f = () => new Promise(function(resolve, reject) {
throw new Error("thrown Error もrejectedされるよ");
}
)
f() //Uncaught (in promise) Error: thrown Error もrejectedされるよ
このように、rejectされなくてもexecuterやthenハンドラでエラーが起きた場合には、それはrejectとして扱われ、一番近いcatchにて処理ができる。
async関数における処理の順序、awaitがある場合とない場合
awaitにおけるエラーハンドリング
本題である、awaitではtry-catchにてエラー処理が実行できるようになり、比較的楽にコードを書けるようになる。
const sleep1sec = () => new Promise((resolve, reject) => {
setTimeout(() => {
console.log('awaitされているPromiseの中')
reject(new Error("awaitでtry-catchできるはず"))
}, 1000) //1秒後にrejectするPromise
})
const awaitf = async() => {
console.log('await前')
try {
await sleep1sec()
} catch {
console.log('try-catch!')
} finally {
console.log('await終了')
}
}
awaitf()
console.log('同期処理')
// await前
// 同期処理
// awaitされているPromiseの中
// try-catch!
// await終了
ここで見ておいてほしいのが、async関数はawaitに出会うとawaitされているPromiseを実行し、それ以降のそれ以降の処理をマイクロタスクキューに送る。そのため、実行の順番は
async関数の中でawaitより前の部分
→async関数が実行されているプロセスにおける同期的な実行 & awaitされているPromise
→awaitより後のasync関数の中身
となる。
ここで、(わたしのように、)うっかりawaitを忘れるとどうなるだろうか?
以下のコードを見て、実行順番がどう変わるか考えてみてください。
const forgetAwait = async() => {
console.log('await前')
try {
sleep1sec() //await忘れちゃった!
} catch {
console.log('try-catch!')
} finally {
console.log('await終了')
}
}
forgetAwait()
console.log('同期処理')
// await前
// await終了
// 同期処理
// awaitされているPromiseの中
// Uncaught (in promise) Error: awaitでtry-catchできるはず<-エラーがキャッチされていない!
繰り返しになるが、awaitにたどり着くとasync関数におけるそれ以降の処理をマイクロタスクキューに送るため、awaitなしでは同期的に処理を行うことになる。
この場合、forgetAwait()においてPromiseの処理が完了されるのを待つことなくcatch文まで処理が進むため、エラーはforgetAwait関数の後に発生する。よってcatchすることはできず、エラーが発生する。
エラー処理を外側に伝播していく方法
ここからさらに、awaitfとforgetfからtry/catch文を除いたものを、それぞれasync関数でtry/catchにてエラーをキャッチしようとすると、catchできるだろうか?
const awaitWithoutCatch = async() => {
console.log('await前')
await sleep1sec()
console.log('await終了')
}
const forgetAwaitWithoutCatch = async() => {
console.log('await前')
sleep1sec() //await忘れちゃった!
console.log('await終了')
}
const awaitTry = async() => {
try {
await awaitWithoutCatch()
} catch {
console.log('catch!')
}
}
const withoutAwaitTry = async() => {
try {
await forgetAwaitWithoutCatch()
} catch {
console.log('catch!')
}
}
awaitTry()
// await前
// awaitされているPromiseの中
// catch! <- catchできている!
withoutAwaitTry()
// await前
// await終了
// awaitされているPromiseの中
// Uncaught (in promise) Error: awaitでtry-catchできるはず <-エラー発生
何度も繰り返したように、awaitされていないPromiseはtry文でcatchすることはできない。対して、awaitされているPromiseによるrejectは、最初のawaitでcatchされなくても(もしくはcatchされた上でthrow Errorされていると)外側のasync関数内におけるtry/catchにてエラーハンドリングができる。これはかなり読みにくい処理になりかねないので、エラーが発生するPromiseを使う場合には必ずcatchでエラー処理をしておくか、catchした後にerrorを投げておくことで、そのPromiseを利用する側にエラーハンドリングの必要性を強調しておく必要があると考える。
所感
awaitを忘れて、さらにasyncでラップしていたためにエラーの発生源を突き止めるのに時間がかかってしまった。これを機にいろいろとjsにおける非同期処理について調べてみた。結局のところブラウザ上のJSはシングルスレッドなので、処理の順番を自分の思い通りにできるようになることが大事な気がする。
参考
元々が日本語で書かれている貴重な資料であることもあって、非常に読みやすくわかりやすい。ボリュームもしっかりあるのでこれをちゃんと理解できればPromiseを使う際に困らなくなると思います
本文中のコードを実際に実行したり、編集した上で実行したりできる。内容がわかりやすいのはもちろんこの時はどうなるんだろう?と簡単に試しながら読み進めていけるので、考え方がより早く身に付く気がします
GIF画像でPromise&Async/Awaitの仕組みを解説してくれるので、めちゃくちゃわかりやすいです。これで私は実行順序を理解できるようになりました。他の解説もすごく良い。
以下はMDN、結局一番網羅的ではあるが、いかんせん読みにくい。気になった時に都度都度繰り返し読むのが良いし、まずは他のところで基礎的なことを理解してからじゃないと厳しい気がします