在這篇中,我們要來看 Effect 中的兩種錯誤類型:預期的錯誤 Expected Error
與非預期的錯誤 Unexpected Error
,另外看在 Effect 中怎麼樣的處理錯誤
在 Effect 中,錯誤被分成兩種,平常我們在 Effect 的 type: Effect<A, E, R>
中看到的 E
指的是預期的錯誤,也就是說,這些錯誤是開發人員預想到可能會發生的錯誤類型,也是你視情況需要去處理的類型,以 fetch
而言,可能就會有個 HttpError
代表著送 request 時發生錯誤,不論是因為網路還是什麼原因
// 這邊刻意的省略了第三個 type 參數,這不是錯誤,而是當 R 為 never 時可以略過不填
function fetchData(): Effect<DataType, HttpError> {}
那麼,非預期的錯誤就如同字面意思一樣,這不是開發者預期會發生的錯誤,所有的沒有在 E
中宣告的錯誤都是非預期的錯誤,有時這種錯誤在 Effect 的文件中也被稱為 defect
,它們有著不同的處理方式
常見的處理錯誤我們可能有幾種不同的策略:
以上也不一定只能使用一種,我也也可以同時顯示錯誤訊息跟重試,這邊先展示前兩種,關於第三種重試的方法,我們之後會再來看
在 Effect 中最簡單的錯誤處理方法是 Effect.catchAll
這會捕捉所有的錯誤
const recoverFromErrorEffect = pipe(
Effect.fail(new Error('something wrong')),
Effect.catchAll((error) => {
console.error('Error:', error) // 顯示錯誤
return Effect.succeed(42) // 需要回傳一個 effect ,這會變成這個 effect 的回傳值
})
)
像上面這樣,我們同時 log ,也回傳了一個預設值
另外因為這邊是預期的錯誤,實際上我們還多了一種處理方法,那就是將錯誤改視為不可預期的錯誤,這通常而言是更嚴重的錯誤代表需要終止程式,例如你的程式高度的依賴 AI 的 api ,若 AI 呼叫失敗了,那後面也不用執行了,那你可以用 orDie
將錯誤升級
// 經過 `orDie` 轉換的 function ,在錯誤這邊一定是 never
const effect: Effect<AIResponse, never> = pipe(
callAI(),
Effect.orDie,
)
通常而言,你不應該處理非預期的錯誤,因為一般來說,未預期的錯誤都是嚴重的錯誤,應該要終止程式的執行,不過我們可能還是有些情況需要處理非預期的錯誤的,例如說你希望顯示給使用者的訊息不要包含太多的錯誤的技術細節,或是需要做錯誤回報時,這時可以使用 Effect.catchAllDefect
const effect = pipe(
// Effect.die 可以用來直接產生非預期錯誤
Effect.die(new Error('fatal error')),
Effect.catchAllDefect((error) => {
reportError(error)
console.error(error)
return Effect.void // 這邊同樣要回傳一個 Effect ,我們回傳空值
})
)
在 Effect 中,所有不是透過 Effect.fail
或是使用 Effect.try*
相關的方法產生的錯誤都會變成非預期的錯誤,例如
const fatalErrorEffect = Effect.sync(() => {
throw new Error('oh no')
})
console.log(Effect.runSyncExit(fatalErrorEffect))
這個你會看到它輸出
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Die',
defect: Error: oh no
...
}
}
這邊你看到的是 Effect 中的一個叫 Cause
的資料型別,只要 Effect 中止,都會有一個 cause 告訴你原因,這個原因有可能是刻意的中斷、有未處理的錯誤、發生嚴重錯誤等。總之,這邊的 Die
代表的是程式碰到非預期的錯誤,跟你在上面看到的 Effect.orDie
是不是可以聯想起來呢?
這其實是 TypeScript 的問題,當 Effect 表示可能有多種預期的錯誤時是像這表示的
type MyEffect = Effect<void, ErrorA | ErrorB>
注意中間的 ErrorA | ErrorB
的部份,這個在 TypeScript 裡叫 union type ,可是這個 union type 有個問題,如果 union 裡有一個 type 是另一個 type 的 super set 的話,也就是說,假設 A 當中包含了 B 的話, A | B
會等於 A
,這樣說可能有點難懂,我們看個例子
// foo 開頭的字串
type B = `foo${string}`
// 所有的字串
type A = string
// C 的 type 是 string
type C = A | B
上面的例子應該挺明顯的, B 是 foo
開頭的字串,那 B 是不是也屬於字串,也就是 A 的型態,也就是 A 是 B 的 super set ,這時 A | B
就會是 A
,這可以到 TypeScript playground 去試試
這時就有一個問題了, unknown
是所有 type 的 super set ,也就是任何的 type 跟 unknown
做 union 後,都會變成 unknown
,那你的 error type 就直接不見了,我們來看以下的範例
// Effect<never, Error>
const errorEffect1 = Effect.fail(new Error('error1'))
// 這就是之前說要避免的情況,這個的 type 是 Effect<never, unknown>
const errorEffect2 = Effect.try({
try: () => {
throw new Error('error2')
},
// 這邊沒有做轉型
catch: (error) => error,
})
// 這個的 type 是 Effect<never, unknown>
const errorEffect = pipe(
errorEffect1,
Effect.flatMap(() => errorEffect2),
)
上面的例子你也可以試看看把 errorEffect1
與 errorEffect2
對調看看,一樣會 E
會是 unknown ,這樣我們就無法知道這個 Effect 會產生什麼錯誤了
這篇我們簡單介紹了 Effect 中的錯誤處理,接下來介紹怎麼建立自訂的錯誤型別