はじめに
エラーハンドリングって大変ですよね。
非同期がはいってくるともっと大変になります。
さいきんそう思うことがあって調べていたら、いい感じの書き方を見つけました。
ここでは try-catch ではなくて await-catch を使って非同期のエラーハンドリングを書くことで、すっきりした書き方になることを目指します。
TL;DR
これが
try {
const response = await callApi()
apiSuccess = true
console.log('res is', response)
} catch(e) {
if (e instanceof TypeError) {
console.log('handle type')
} else if (e instanceof SyntaxError) {
console.log('handle syntax')
} else {
throw e
}
} finally {
console.log('success?', apiSuccess)
}
こうなります
const response = await callApi()
.then(() => { apiSuccess = true })
.catch(handleType)
.catch(handleSyntax)
.catch(Promise.reject)
.finally(() => {
console.log('success?', apiSuccess)
})
console.log('res is', response)
まずは同期処理から
通常のエラーハンドリングはこんな感じになるかと思います。
まずは同期処理ですが、以降の例でこれを変えていきます。
function callApi() {
throw new SyntaxError('a')
}
let apiSuccess = false
try {
const response = callApi()
apiSuccess = true
console.log('res is', response)
} catch(e) {
if (e instanceof TypeError) {
console.log('handle type')
} else if (e instanceof SyntaxError) {
console.log('handle syntax')
} else {
throw e
}
} finally {
console.log('success?', apiSuccess)
}
API を呼ぶときは非同期にしたい
API は非同期で呼びたいですね。
なので非同期にします。
async function callApi() { // async にする
throw new SyntaxError('a')
}
let apiSuccess = false
try {
const response = await callApi() // await にする
apiSuccess = true
console.log('res is', response)
} catch(e) {
if (e instanceof TypeError) {
console.log('handle type')
} else if (e instanceof SyntaxError) {
console.log('handle syntax')
} else {
throw e
}
} finally {
console.log('success?', apiSuccess)
}
await
がないと catch
されずにエラーが突き抜けるので注意です。
await の try-catch といえば(本題)
さて、 javascript の await
はメソッドチェーンでエラーハンドルができます。
するとどうなるか。コードがすっきりして嬉しいです。
というわけで、同期処理の try-catch を非同期処理の書き方に変換します。
throw
についてはこのままでも動きますが、await
にあわせて Promise.reject
を使います。
throw を Promise.reject にする
async function callApi() {
throw new SyntaxError('a')
}
// throw を Promise.reject にする
async function callApi() {
return Promise.reject(new SyntaxError('a'))
}
try
を変換する
try
の処理を、[ throw するもの] と [throw しないもの] と [戻り値を扱うもの] に分けます。
throw をするものはそのまま書いて、throw をしないものは .then()
に書きます。
もし処理の中で throw されると .then()
は無視して、直近の .catch()
にはいります。
また、戻り値を扱う処理はメソッドチェーンのあとに書きます。
try {
const response = await callApi()
apiSuccess = true
console.log('res is', response)
}
// ↓ try を消す ↓
const response = await callApi() // throw するもの
.then(() => { apiSuccess = true }) // throw しないもの
// 戻り値を扱うもの
console.log('res is', response)
catch
を変換する
catch
の中の処理を書き換えます。
メソッドチェーンにしたいので、エラーを引数にしたメソッドに切り出します。
今回の例は処理が複数あるので、メソッドを複数個つくっています。
catch(e) {
if (e instanceof TypeError) {
console.log('handle type')
} else if (e instanceof SyntaxError) {
console.log('handle syntax')
} else {
throw e
}
}
// ↓ if (...) { ... } を非同期関数にする ↓
async function handleType(e) {
if (e instanceof TypeError) {
console.log('handle type')
} else {
return Promise.reject(e)
}
}
async function handleSyntax(e) {
if (e instanceof SyntaxError) {
console.log('handle syntax')
} else {
return Promise.reject(e)
}
}
// ↓ catch を消す ↓
.catch(handleType)
.catch(handleSyntax)
.catch(Promise.reject)
finally
を変換する
finally {
console.log('success?', apiSuccess)
}
// ↓ finally を変換する ↓
.finally(() => {
console.log('success?', apiSuccess)
})
変換した結果
というわけで、まとめると以下のようになります。
変換前
async function callApi() {
throw new SyntaxError('a')
}
let apiSuccess = false
try {
const response = await callApi()
apiSuccess = true
console.log('res is', response)
} catch(e) {
if (e instanceof TypeError) {
console.log('handle type')
} else if (e instanceof SyntaxError) {
console.log('handle syntax')
} else {
throw e
}
} finally {
console.log('success?', apiSuccess)
}
変換後
async function handleType(e) {
if (e instanceof TypeError) {
console.log('handle type')
} else {
return Promise.reject(e)
}
}
async function handleSyntax(e) {
if (e instanceof SyntaxError) {
console.log('handle syntax')
} else {
return Promise.reject(e)
}
}
async function callApi() {
return Promise.reject(new SyntaxError('a'))
}
let apiSuccess = false
const response = await callApi()
.then(() => { apiSuccess = true })
.catch(handleType)
.catch(handleSyntax)
.catch(Promise.reject)
.finally(() => {
console.log('success?', apiSuccess)
})
console.log('res is', response)
記述量が増えた?その通りです。
ですが、見やすくなるので私はこちらの方が好きです。こっちのほうがスマートじゃない?
あと handleType
や handleSyntax
は他のエラーハンドリングでも使いまわせるので、総合的には記述量が減ることを見込めます。
締め
こちらを参考に書きました
もっといい方法がありましたら教えてください。