為什麼需要 Effect
程式中的副作用
在程式開發中,「副作用」指會影響外部世界的操作,例如記錄日誌、發送網路請求、存取資料庫、寫入檔案,甚至 console.log。相對地,在記憶體中計算 1 + 1 則屬於不影響外界的純計算。
因此,程式若要真正「有用」,就離不開副作用;沒有輸出、沒有持久化、沒有畫面,軟體就毫無價值。
副作用的麻煩事
副作用讓系統「能做事」,但也最容易失控:
- 容易出現未處理的錯誤。
- 非同步流程難管理(取消、超時、重試)。
- 資源釋放常被忽略(例如遺漏資料庫連線釋放)。
- 邏輯分散,導致難以測試與維護。
真正棘手的,往往不是程式本身是否正確運行,而是副作用伴隨而來的錯誤、併發與資源管理問題。
我們需要 Effect 的原因
要打造生產等級(production‑grade)的軟體,妥善處理副作用是不可避免的;而 Effect 提供一種宣告式(declarative)的方法來處理它。
在程式中,我們用泛型外觀 Effect<Success, Error, Requirements> 來描述一段計算:它在某個需求環境 (Requirements) 中執行,成功時產生 Success,失敗時產生 Error。要注意的是,Effect 是 effect library 定義的一種函數式資料型別 (data type)。它不是 TypeScript 內建的型別,而是用來抽象描述「副作用運算」的型別結構。所以雖然在 TypeScript 裡 Effect 確實還是個「型別」,但在函數式編程(FP)的語境,會特別稱它為「資料型別 (data type)」,因為它表達的不只是靜態結構,而是一個「可組合、帶行為的抽象」。
此外,Effect 是
1.「惰性(lazy)」的,只描繪要執行的過程,不直接執行。
2. 具有組合性,可以與其他 Effect 進行組合,組合後的結果也會是一個 Effect。
3. 具有 immutable(不可變)的特性,一旦被定義,程式要如何運行就被確定了,不會再改變。
因為上面三個特性,讓我們可以先以宣告式的方式組裝流程,之後再透過 Effect 模組提供的各種 run function 來執行。這也讓我們在錯誤處理和依賴資源的管理上,可以更輕鬆準確的控制。
傳統作法的侷限
傳統的 Promise 與 async/await 確實改善了非同步的可讀性,但仍有明顯限制:
- 資源釋放無保證:等待結果容易,但難保過程失敗時一定釋放資源(例如檔案/連線)。
- 錯誤型別不明確:
try/catch能攔錯,但型別系統不會告訴你「可能發生哪些錯誤、哪些需要處理」。 - 缺乏一致模型:難以在型別/介面層面描述需求(設定、連線、權限),也難預告潛在失敗點。
- 跨功能的一致作法難落地:像取消、超時、重試、觀測和資源釋放,常散落在各處、寫法各一套,難以統一與重用。
Effect 是什麼
Effect 的設計理念是:既然副作用無法避免,就把它變成「可描述的管線節點」來組裝起來,讓副作用以可描述、可推理、可組合的方式存在。
Effect 的型別哲學
Effect 提供三個泛型型別 Effect<Success, Error, Requirements>:
Success:成功回傳的結果(成功通道)Error:可能失敗的錯誤類型(錯誤通道)Requirements:這個副作用需要什麼(環境 / 依賴)
小詞彙表(例子):
Success範例:User、number、voidError範例:NotFoundError、TimeoutError、PermissionErrorRequirements範例:記錄器(Logger)、資料庫連線、環境設定(Config)
把副作用封裝成 Effect<Success, Error, Requirements> 後,你可以在型別層面精準描述需求與風險,並用一組可重用的操作來安全地串接、並行、重試、超時,並確保資源取得與釋放。程式從「滿地亂飛的副作用」變成「一段段可描述、可組裝的生產線」。
五個關鍵價值
- 錯誤處理(雙通道與型別化):成功與失敗被清楚分開、同等對待;錯誤也會被標示在型別裡。當流程由多個步驟組成時,所有可能的錯誤會被彙整並向上標示,讓開發者在編譯期就能看見並決定如何處理。
- Lazy evaluation 與 Aspect-Oriented Programming (AOP):Effect 延遲執行,使「重試、退避、超時、熔斷、快取、批次」等橫切關注(cross-cutting concerns)能在「執行之前」以組合方式掛載,達到類 AOP 的效果而不污染商業邏輯。
- 依賴注入(Dependency Injection, DI):以
requirements顯式表達需求,呼叫端不必了解底層實作或連線細節,只要聲明需要的服務即可。 - 上下文傳遞(Context as dependency):像
requestId、Logger這類每個請求才會有的資訊,不需要依賴 AsyncLocalStorage(Node.js 提供的全域非同步儲存 API)。Effect 採用 service 抽象來管理這些依賴,你只需在請求的入口設定一次,內層邏輯需要時可直接從 Context 取出,而不用把參數一路往下傳遞。 - 測試友善:可直接注入「函數」或最小替身,避免為了 stub 而攜帶整個類別;亦可用 Layer 在建立服務時一次性提供替身,讓單元測試與整合測試更輕量且可預測。
更好的非同步錯誤處理機制與資源管理機制(這裡講的比較抽象,未來會深度講這個問題)
- Promise:像是一條沒有標準作業流程的生產線,產品能推出來就算完成;但一出狀況,很難在過程中觀察、控制與介入,呼叫方也很難從介面看出需要哪些前置條件(如設定、連線、權限),或預測可能的失敗點。
- Effect:像是一條有標準作業流程與檢查點的生產線,每個站點都明確記錄「需要的前置條件、可能的失敗、與輸出的結果」,還能加上開關(取消)、計時(超時)、備援(重試/回退),並確保收尾時會做清場(資源釋放)。
何時不需要 Effect
不是每個情境都值得導入 Effect。當你追求的是最短開發時間,或問題本身非常單純時,Effect 反而可能成為額外負擔。幾個常見情況如下。
首先,如果你的程式只涉及純計算或單純資料轉換(沒有任何 I/O),像是字串與日期處理、資料驗證或演算法實作,Effect 帶來的抽象就未必合算。這類邏輯本來就容易以純函式表達,可靠性問題也有限。
其次,在一次性腳本、極簡 demo、或短命的 POC 階段,目標是「快速得到回饋」。此時長期的可靠性、觀測性與可維護性不是優先,導入 Effect 的學習與設置成本通常超過收益。
再者,若問題空間很簡單,或外部平台已提供你需要的穩定機制(如自動重試、超時、熔斷,就不必再自建一套 Effect 化的可靠性模型;把握邊際效益即可。有一種常見情境是「邊界很薄,且由框架/SDK 接管副作用」。所謂接管,指的是 SDK 內部已處理了 HTTP/連線池、認證、重試、超時與錯誤分類,並以一致的回傳格式暴露結果。你的程式只負責把請求參數交給 SDK,再把回傳值往上層傳遞。例如呼叫雲端儲存或金流的官方 SDK:你通常只需 await sdk.doSomething(),其間的網路可靠性、資源管理與錯誤分類多半已被 SDK 標準化。若你的業務邏輯在這層幾乎不加工,於此處再以 Effect 建模,就可能沒有太多實質收益。
最後,還有組織面的考量:若團隊目前不具備 Effect 經驗,或專案處於高度趕工階段,學習與遷移成本可能壓過短期回報。此時維持現狀,並在關鍵路徑做最小增量優化,通常更務實。
實務上也可以採「混合導入」:把 Effect 用在 I/O 密集、需要取消/超時/重試/資源安全保障的核心路徑;其他輕量路徑就維持現有的 Promise/async 寫法,避免全面改寫的風險。
簡易判斷清單:
- 這段程式是否包含 I/O,且需要取消、超時、重試或嚴格的資源釋放?
- 是否希望在編譯期就看到並統一處理錯誤型別(而非
unknown)? - 是否需要跨層/跨請求的上下文傳遞(如
Logger、requestId、權限)? - 是否需要更容易的替身注入與可組合的 DI?
- 若以上多數回答為「否」,此處先不導入 Effect 也可。
Effect 的成長趨勢
在學習一個新工具時,了解趨勢能幫助我們保持動力。下圖為 GitHub 上 Effect 的星標數趨勢;自 2023 年下半年起成長明顯,值得關注與評估。

下一步
接下來我們會正式開始講解 Effect 的語法。
參考資料:
Last updated on