iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0

在這篇中,我們要來看 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 ,它們有著不同的處理方式

處理預期的錯誤

常見的處理錯誤我們可能有幾種不同的策略:

  1. 顯示錯誤訊息,讓使用者知道
  2. 回傳預設值,或是代表錯誤的特殊值
  3. 重試

以上也不一定只能使用一種,我也也可以同時顯示錯誤訊息跟重試,這邊先展示前兩種,關於第三種重試的方法,我們之後會再來看

在 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 是不是可以聯想起來呢?

為什麼 error 需要 type

這其實是 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),
)

上面的例子你也可以試看看把 errorEffect1errorEffect2 對調看看,一樣會 E 會是 unknown ,這樣我們就無法知道這個 Effect 會產生什麼錯誤了

這篇我們簡單介紹了 Effect 中的錯誤處理,接下來介紹怎麼建立自訂的錯誤型別

Reference


上一篇
5. 初識 Effect 中的 concurrency
系列文
Effect 魔法:打造堅不可摧的應用程式7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言