大家好,因為上次的Golang核心處理的文章需要study的部分非常多,所以我還得再花些時間了解,再請大家見諒((´д`)),本週會先介紹Golang與Node.js錯誤處理可以討論與借鑑的地方
你是否有寫過以下的code
try {
await request()
} catch (error) {
// 處理request的error程序...
}
在Node.js裡面使用try catch可以妥善的保護程式碼,讓request()這個非同步函式出錯時可以直接跳到catch處執行例外處理,那問題來了,如果現在有多個request()呢?
try {
await request1()
await request2()
await request2()
} catch (error) {
// 處理request的error程序...
}
這時候發生了錯誤,當log印出
我因為timeout產生error而爆炸了
我們會發現一件事
到底是哪個request timeout了
三個request回傳的error都有可能是timeout,是的,我們的確可以從stack log看出是哪個錯誤,但在對應處理上我們無法區分,有些人會直接回傳timeout message,但這message就有三種可能性,這樣的處理方案有時候就像提供錯誤資訊般。
我這邊依序列出我一路解決的方法:
try {
await request1()
catch (error) {
// 處理request1的error程序...
}
try {
await request2()
catch (error) {
// 處理request2的error程序...
}
try {
await request3()
catch (error) {
// 處理request3的error程序...
}
這的確解決了我的問題,我可以精準的對不同request進行錯誤處理,但這程式碼不覺得很醜嗎?這樣的使用似乎跟callback hell的難閱讀有幾分相似,非常的難閱讀。
async request1() {
try {
// request的程序...
} catch (error) {
if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
throw new customError1('TypeError')
}
else if (error.name === '其他error name' && error.message === '其他error message') {
throw new customError1('其他error')
}
// ...許多的else if
}
}
// request2, request3也都是各拋出自己的自訂error
try {
await request1() // 出錯會拋出customError1
await request2() // 出錯會拋出customError2
await request2() // 出錯會拋出customError3
} catch (error) {
if (error instanceof customError1) {
if (error.name === 'TypeError') {
// ...處理request1的`TypeError`程序
}
else if (error.name === '其他error name') {
// ...處理request1的`其他error name`程序
}
}
else if (error instanceof customError2) {
// ...處理request2的error程序
}
else if (error instanceof customError2) {
// ...處理request3的error程序
}
}
這解決的難閱讀的問題,但又引來了另個問題,就是除了我要自訂這些error以外,我在request裡面也得分析他原始的error,並且重新賦予給自訂error,這其實有點麻煩,因為我的程式碼會看到滿滿的自訂error
const result1 = await request1().catch(error => error)
if (result1 instanceof Error) {
// 處理request1的error程序...
}
const result2 = await request2().catch(error => error)
if (result2 instanceof Error) {
// 處理request2的error程序...
}
const result3 = await request3().catch(error => error)
if (result3 instanceof Error) {
// 處理request3的error程序...
}
好,我們可以不再自訂那麼多error了,但現在result有「成功與失敗」兩種邏輯,這很惱人,這導致了變數的職責很不單一,這個result實際上應該取名resultOrError,但這讓一切變得很奇怪
Golang的錯誤處理中,鼓勵大家不使用try catch來處理,而是「好好定義會回傳什麼錯誤」
resp, err := http.Get("http://example.com/")
if err != nil {
// handle error
}
原來可以這樣,我們把這方法拿回Node.js
async request1() {
try {
// request的程序...
return [result, null]
} catch (error) {
// ..處理error的程序
return [null, finalError]
}
}
// request2, request3也都是回傳陣列
const [result1, error1] = await request1()
if (error1) {
// 處理request1的error程序...
}
const [result2, error2] = await request2()
if (error2) {
// 處理request2的error程序...
}
const [result3, error3] = await request3()
if (error3) {
// 處理request3的error程序...
}
這樣做可以很直接的解決在多個requests之下,出現了什麼錯誤必須要怎麼處理的事情,而回傳值的職責也更為分明,並且也相當簡單。
我目前很喜歡這樣的做法,坦白說,這樣的做法很類似於以前的callback:
// callback
function(err, result) {
if (error) {
//do error handle
}
// do something
}
我們單純把callback hell解決,並且也能精確定位error,而github上也有人喜愛此作法,並且把這做法寫成了一個await-to-js套件,大家可以下載使用
不過,這與許多的語言不同,而這邊也有幾個有趣的例子可以拿來討論。
Stack Overflow創始者的Joel Spolsky大神認為,使用try catch無疑會造成程式碼的一種「跳躍」,這使得我們要對這些例外處理要處理特定狀況變得很困難。
而這種跳躍的意思就是類似於"goto",在try範圍內對外發送requests時,如果出現了錯誤,那會直接跳到catch的範圍,如果你不說好這些跳到catch範圍的error到底是什麼,那你就會面臨跟goto一樣的狀況,「哇賽,現在程式碼進到goto到底是為什麼」
而在與朋友討論時,曾被問到:「重構一書的Martin Fowler大神在書中提到,程式碼應該要可以逐行了解意圖,我想問的是,使用Golang這樣的方法是不是把「正向的邏輯」與「例外的邏輯」寫在了同一個段落呢?」
我個人認為,這是關於設計者怎麼去看待這個例外,
有些人認為:
既然是例外了,那本身就屬於不正常的事,那就統一處理這類的例外。
有些人認為:
雖然他是一個程序上的例外,但他存在我整體的流程中,所以也是正向邏輯的一環。
而已我來說,我偏向於後者,雖然我不到經驗老到,但我碰到許多的狀況是
同種例外我需要做不同的處理
正如文章開頭timeout的例子,不同request的timeout其實對我整個程序的流程是需要有不同的處理的,
所以與其說「把例外寫在正向邏輯裡會導致邏輯不清楚」,不如說「如果能夠配合更好的邏輯分層,把例外處理算為正向邏輯的一環,其實更能好好的處理邏輯」
大家是怎麼去看待錯誤邏輯的?我的想法是否與你的想法不同?歡迎大家討論,謝謝~