@dojocho/effect-ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/DOJO.md +22 -0
  2. package/dojo.json +50 -0
  3. package/katas/001-hello-effect/SENSEI.md +72 -0
  4. package/katas/001-hello-effect/solution.test.ts +35 -0
  5. package/katas/001-hello-effect/solution.ts +16 -0
  6. package/katas/002-transform-with-map/SENSEI.md +72 -0
  7. package/katas/002-transform-with-map/solution.test.ts +33 -0
  8. package/katas/002-transform-with-map/solution.ts +16 -0
  9. package/katas/003-generator-pipelines/SENSEI.md +72 -0
  10. package/katas/003-generator-pipelines/solution.test.ts +40 -0
  11. package/katas/003-generator-pipelines/solution.ts +29 -0
  12. package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
  13. package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
  14. package/katas/004-flatmap-and-chaining/solution.ts +18 -0
  15. package/katas/005-pipe-composition/SENSEI.md +81 -0
  16. package/katas/005-pipe-composition/solution.test.ts +41 -0
  17. package/katas/005-pipe-composition/solution.ts +19 -0
  18. package/katas/006-handle-errors/SENSEI.md +86 -0
  19. package/katas/006-handle-errors/solution.test.ts +53 -0
  20. package/katas/006-handle-errors/solution.ts +30 -0
  21. package/katas/007-tagged-errors/SENSEI.md +79 -0
  22. package/katas/007-tagged-errors/solution.test.ts +82 -0
  23. package/katas/007-tagged-errors/solution.ts +37 -0
  24. package/katas/008-error-patterns/SENSEI.md +89 -0
  25. package/katas/008-error-patterns/solution.test.ts +41 -0
  26. package/katas/008-error-patterns/solution.ts +38 -0
  27. package/katas/009-option-type/SENSEI.md +96 -0
  28. package/katas/009-option-type/solution.test.ts +49 -0
  29. package/katas/009-option-type/solution.ts +26 -0
  30. package/katas/010-either-and-exit/SENSEI.md +86 -0
  31. package/katas/010-either-and-exit/solution.test.ts +33 -0
  32. package/katas/010-either-and-exit/solution.ts +17 -0
  33. package/katas/011-services-and-context/SENSEI.md +82 -0
  34. package/katas/011-services-and-context/solution.test.ts +23 -0
  35. package/katas/011-services-and-context/solution.ts +17 -0
  36. package/katas/012-layers/SENSEI.md +73 -0
  37. package/katas/012-layers/solution.test.ts +23 -0
  38. package/katas/012-layers/solution.ts +26 -0
  39. package/katas/013-testing-effects/SENSEI.md +88 -0
  40. package/katas/013-testing-effects/solution.test.ts +41 -0
  41. package/katas/013-testing-effects/solution.ts +20 -0
  42. package/katas/014-schema-basics/SENSEI.md +81 -0
  43. package/katas/014-schema-basics/solution.test.ts +35 -0
  44. package/katas/014-schema-basics/solution.ts +25 -0
  45. package/katas/015-domain-modeling/SENSEI.md +85 -0
  46. package/katas/015-domain-modeling/solution.test.ts +46 -0
  47. package/katas/015-domain-modeling/solution.ts +42 -0
  48. package/katas/016-retry-and-schedule/SENSEI.md +72 -0
  49. package/katas/016-retry-and-schedule/solution.test.ts +26 -0
  50. package/katas/016-retry-and-schedule/solution.ts +23 -0
  51. package/katas/017-parallel-effects/SENSEI.md +70 -0
  52. package/katas/017-parallel-effects/solution.test.ts +33 -0
  53. package/katas/017-parallel-effects/solution.ts +17 -0
  54. package/katas/018-race-and-timeout/SENSEI.md +75 -0
  55. package/katas/018-race-and-timeout/solution.test.ts +30 -0
  56. package/katas/018-race-and-timeout/solution.ts +27 -0
  57. package/katas/019-ref-and-state/SENSEI.md +72 -0
  58. package/katas/019-ref-and-state/solution.test.ts +29 -0
  59. package/katas/019-ref-and-state/solution.ts +16 -0
  60. package/katas/020-fibers/SENSEI.md +80 -0
  61. package/katas/020-fibers/solution.test.ts +23 -0
  62. package/katas/020-fibers/solution.ts +23 -0
  63. package/katas/021-acquire-release/SENSEI.md +57 -0
  64. package/katas/021-acquire-release/solution.test.ts +23 -0
  65. package/katas/021-acquire-release/solution.ts +22 -0
  66. package/katas/022-scoped-layers/SENSEI.md +52 -0
  67. package/katas/022-scoped-layers/solution.test.ts +35 -0
  68. package/katas/022-scoped-layers/solution.ts +19 -0
  69. package/katas/023-resource-patterns/SENSEI.md +52 -0
  70. package/katas/023-resource-patterns/solution.test.ts +20 -0
  71. package/katas/023-resource-patterns/solution.ts +13 -0
  72. package/katas/024-streams-basics/SENSEI.md +61 -0
  73. package/katas/024-streams-basics/solution.test.ts +30 -0
  74. package/katas/024-streams-basics/solution.ts +16 -0
  75. package/katas/025-stream-operations/SENSEI.md +59 -0
  76. package/katas/025-stream-operations/solution.test.ts +26 -0
  77. package/katas/025-stream-operations/solution.ts +17 -0
  78. package/katas/026-combining-streams/SENSEI.md +54 -0
  79. package/katas/026-combining-streams/solution.test.ts +20 -0
  80. package/katas/026-combining-streams/solution.ts +16 -0
  81. package/katas/027-data-pipelines/SENSEI.md +58 -0
  82. package/katas/027-data-pipelines/solution.test.ts +22 -0
  83. package/katas/027-data-pipelines/solution.ts +16 -0
  84. package/katas/028-logging-and-spans/SENSEI.md +58 -0
  85. package/katas/028-logging-and-spans/solution.test.ts +50 -0
  86. package/katas/028-logging-and-spans/solution.ts +20 -0
  87. package/katas/029-http-client/SENSEI.md +59 -0
  88. package/katas/029-http-client/solution.test.ts +49 -0
  89. package/katas/029-http-client/solution.ts +24 -0
  90. package/katas/030-capstone/SENSEI.md +63 -0
  91. package/katas/030-capstone/solution.test.ts +67 -0
  92. package/katas/030-capstone/solution.ts +55 -0
  93. package/katas/031-config-and-environment/SENSEI.md +77 -0
  94. package/katas/031-config-and-environment/solution.test.ts +38 -0
  95. package/katas/031-config-and-environment/solution.ts +11 -0
  96. package/katas/032-cause-and-defects/SENSEI.md +90 -0
  97. package/katas/032-cause-and-defects/solution.test.ts +50 -0
  98. package/katas/032-cause-and-defects/solution.ts +23 -0
  99. package/katas/033-pattern-matching/SENSEI.md +86 -0
  100. package/katas/033-pattern-matching/solution.test.ts +36 -0
  101. package/katas/033-pattern-matching/solution.ts +28 -0
  102. package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
  103. package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
  104. package/katas/034-deferred-and-coordination/solution.ts +24 -0
  105. package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
  106. package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
  107. package/katas/035-queue-and-backpressure/solution.ts +21 -0
  108. package/katas/036-schema-advanced/SENSEI.md +81 -0
  109. package/katas/036-schema-advanced/solution.test.ts +55 -0
  110. package/katas/036-schema-advanced/solution.ts +19 -0
  111. package/katas/037-cache-and-memoization/SENSEI.md +73 -0
  112. package/katas/037-cache-and-memoization/solution.test.ts +47 -0
  113. package/katas/037-cache-and-memoization/solution.ts +24 -0
  114. package/katas/038-metrics/SENSEI.md +91 -0
  115. package/katas/038-metrics/solution.test.ts +39 -0
  116. package/katas/038-metrics/solution.ts +23 -0
  117. package/katas/039-managed-runtime/SENSEI.md +75 -0
  118. package/katas/039-managed-runtime/solution.test.ts +29 -0
  119. package/katas/039-managed-runtime/solution.ts +19 -0
  120. package/katas/040-request-batching/SENSEI.md +87 -0
  121. package/katas/040-request-batching/solution.test.ts +56 -0
  122. package/katas/040-request-batching/solution.ts +32 -0
  123. package/package.json +22 -0
  124. package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
  125. package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
  126. package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
  127. package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
  128. package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
  129. package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
  130. package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
  131. package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
  132. package/skills/effect-patterns-error-management/SKILL.md +1668 -0
  133. package/skills/effect-patterns-getting-started/SKILL.md +237 -0
  134. package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
  135. package/skills/effect-patterns-observability/SKILL.md +1586 -0
  136. package/skills/effect-patterns-platform/SKILL.md +1195 -0
  137. package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
  138. package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
  139. package/skills/effect-patterns-resource-management/SKILL.md +827 -0
  140. package/skills/effect-patterns-scheduling/SKILL.md +451 -0
  141. package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
  142. package/skills/effect-patterns-streams/SKILL.md +2052 -0
  143. package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
  144. package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
  145. package/skills/effect-patterns-testing/SKILL.md +1632 -0
  146. package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
  147. package/skills/effect-patterns-value-handling/SKILL.md +676 -0
  148. package/tsconfig.json +20 -0
  149. package/vitest.config.ts +3 -0
@@ -0,0 +1,1668 @@
1
+ ---
2
+ name: effect-patterns-error-management
3
+ description: Effect-TS patterns for Error Management. Use when working with error management in Effect-TS applications.
4
+ ---
5
+ # Effect-TS Patterns: Error Management
6
+ This skill provides 15 curated Effect-TS patterns for error management.
7
+ Use this skill when working on tasks related to:
8
+ - error management
9
+ - Best practices in Effect-TS applications
10
+ - Real-world patterns and solutions
11
+
12
+ ---
13
+
14
+ ## 🟢 Beginner Patterns
15
+
16
+ ### Pattern Match on Option and Either
17
+
18
+ **Rule:** Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values
19
+
20
+ **Good Example:**
21
+
22
+ ### Basic Option Matching
23
+
24
+ ```typescript
25
+ import { Option } from "effect";
26
+
27
+ const getUserName = (id: number): Option.Option<string> => {
28
+ return id === 1 ? Option.some("Alice") : Option.none();
29
+ };
30
+
31
+ // Using .match() for declarative pattern matching
32
+ const displayUser = (id: number): string =>
33
+ getUserName(id).pipe(
34
+ Option.match({
35
+ onNone: () => "Guest User",
36
+ onSome: (name) => `Hello, ${name}!​`,
37
+ })
38
+ );
39
+
40
+ console.log(displayUser(1)); // "Hello, Alice!"
41
+ console.log(displayUser(999)); // "Guest User"
42
+ ```
43
+
44
+ ### Basic Either Matching
45
+
46
+ ```typescript
47
+ import { Either } from "effect";
48
+
49
+ const validateAge = (age: number): Either.Either<number, string> => {
50
+ return age >= 18
51
+ ? Either.right(age)
52
+ : Either.left("Must be 18 or older");
53
+ };
54
+
55
+ // Using .match() for error handling
56
+ const processAge = (age: number): string =>
57
+ validateAge(age).pipe(
58
+ Either.match({
59
+ onLeft: (error) => `Validation failed: ${error}`,
60
+ onRight: (validAge) => `Age ${validAge} is valid`,
61
+ })
62
+ );
63
+
64
+ console.log(processAge(25)); // "Age 25 is valid"
65
+ console.log(processAge(15)); // "Validation failed: Must be 18 or older"
66
+ ```
67
+
68
+ ### Advanced: Nested Matching
69
+
70
+ When dealing with nested Option and Either, use nested `.match()` calls:
71
+
72
+ ```typescript
73
+ import { Option, Either } from "effect";
74
+
75
+ interface UserProfile {
76
+ name: string;
77
+ age: number;
78
+ }
79
+
80
+ const getUserProfile = (
81
+ id: number
82
+ ): Option.Option<Either.Either<string, UserProfile>> => {
83
+ if (id === 0) return Option.none(); // User not found
84
+ if (id === 1) return Option.some(Either.left("Profile incomplete"));
85
+ return Option.some(Either.right({ name: "Bob", age: 25 }));
86
+ };
87
+
88
+ // Nested matching - first on Option, then on Either
89
+ const displayProfile = (id: number): string =>
90
+ getUserProfile(id).pipe(
91
+ Option.match({
92
+ onNone: () => "User not found",
93
+ onSome: (result) =>
94
+ result.pipe(
95
+ Either.match({
96
+ onLeft: (error) => `Error: ${error}`,
97
+ onRight: (profile) => `${profile.name} (${profile.age})`,
98
+ })
99
+ ),
100
+ })
101
+ );
102
+
103
+ console.log(displayProfile(0)); // "User not found"
104
+ console.log(displayProfile(1)); // "Error: Profile incomplete"
105
+ console.log(displayProfile(2)); // "Bob (25)"
106
+ ```
107
+
108
+ **Anti-Pattern:**
109
+
110
+ Avoid manual conditional checks and nested ternaries:
111
+
112
+ ```typescript
113
+ // ❌ ANTI-PATTERN: Imperative checks with isSome/isLeft
114
+ const name = getUserName(1);
115
+ let result: string;
116
+ if (Option.isSome(name)) {
117
+ result = `Hello, ${name.value}!​`;
118
+ } else {
119
+ result = "Guest User";
120
+ }
121
+
122
+ // ❌ ANTI-PATTERN: Nested ternaries
123
+ const ageResult = validateAge(25);
124
+ const message = ageResult.pipe(
125
+ Either.match({
126
+ onLeft: () => "Invalid",
127
+ onRight: (age) => age >= 21 ? "Can drink" : "Cannot drink",
128
+ })
129
+ );
130
+
131
+ // ❌ ANTI-PATTERN: Chained if-else instead of match
132
+ function processValue(value: Option.Option<number>): string {
133
+ if (Option.isSome(value)) {
134
+ if (value.value > 0) {
135
+ return "Positive";
136
+ } else if (value.value < 0) {
137
+ return "Negative";
138
+ } else {
139
+ return "Zero";
140
+ }
141
+ }
142
+ return "No value";
143
+ }
144
+ ```
145
+
146
+ Why these are worse:
147
+ - **Less readable**: The intent is hidden in imperative logic
148
+ - **Error-prone**: Easy to forget cases or introduce bugs
149
+ - **Mutable state**: Often requires intermediate variables
150
+ - **Less composable**: Harder to pipe and combine operations
151
+
152
+ **Rationale:**
153
+
154
+ When you need to handle `Option` or `Either` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Either) in a single expression.
155
+
156
+ Use `.match()` when:
157
+ - You need to handle both success and failure cases
158
+ - You want type-safe pattern matching
159
+ - You prefer declarative over imperative code
160
+ - You need to transform values based on their case
161
+
162
+
163
+ The `.match()` combinator is superior to manual checks (`isSome()`, `isLeft()`) because:
164
+
165
+ 1. **Declarative**: Expresses intent clearly - "match on these cases"
166
+ 2. **Type-safe**: TypeScript ensures all cases are handled
167
+ 3. **Exhaustive**: You can't accidentally miss a case
168
+ 4. **Composable**: Works naturally with `.pipe()` for chaining operations
169
+ 5. **Readable**: The structure mirrors the data type itself
170
+
171
+ Without `.match()`, you'd need imperative conditionals, which are harder to read and easier to get wrong.
172
+
173
+ ---
174
+
175
+ ### Your First Error Handler
176
+
177
+ **Rule:** Use catchAll or catchTag to recover from errors and keep your program running.
178
+
179
+ **Good Example:**
180
+
181
+ ```typescript
182
+ import { Effect, Data } from "effect"
183
+
184
+ // ============================================
185
+ // 1. Define typed errors
186
+ // ============================================
187
+
188
+ class NetworkError extends Data.TaggedError("NetworkError")<{
189
+ readonly url: string
190
+ }> {}
191
+
192
+ class NotFoundError extends Data.TaggedError("NotFoundError")<{
193
+ readonly resource: string
194
+ }> {}
195
+
196
+ // ============================================
197
+ // 2. Functions that can fail
198
+ // ============================================
199
+
200
+ const fetchData = (url: string): Effect.Effect<string, NetworkError> =>
201
+ url.startsWith("http")
202
+ ? Effect.succeed(`Data from ${url}`)
203
+ : Effect.fail(new NetworkError({ url }))
204
+
205
+ const findUser = (id: string): Effect.Effect<{ id: string; name: string }, NotFoundError> =>
206
+ id === "123"
207
+ ? Effect.succeed({ id, name: "Alice" })
208
+ : Effect.fail(new NotFoundError({ resource: `user:${id}` }))
209
+
210
+ // ============================================
211
+ // 3. Handle ALL errors with catchAll
212
+ // ============================================
213
+
214
+ const withFallback = fetchData("invalid-url").pipe(
215
+ Effect.catchAll((error) => {
216
+ console.log(`Failed: ${error.url}, using fallback`)
217
+ return Effect.succeed("Fallback data")
218
+ })
219
+ )
220
+
221
+ // Result: "Fallback data"
222
+
223
+ // ============================================
224
+ // 4. Handle SPECIFIC errors with catchTag
225
+ // ============================================
226
+
227
+ const findUserOrDefault = (id: string) =>
228
+ findUser(id).pipe(
229
+ Effect.catchTag("NotFoundError", (error) => {
230
+ console.log(`User not found: ${error.resource}`)
231
+ return Effect.succeed({ id: "guest", name: "Guest User" })
232
+ })
233
+ )
234
+
235
+ // ============================================
236
+ // 5. Handle MULTIPLE error types
237
+ // ============================================
238
+
239
+ const fetchUser = (url: string, id: string) =>
240
+ Effect.gen(function* () {
241
+ yield* fetchData(url)
242
+ return yield* findUser(id)
243
+ })
244
+
245
+ const robustFetchUser = (url: string, id: string) =>
246
+ fetchUser(url, id).pipe(
247
+ Effect.catchTags({
248
+ NetworkError: (e) => Effect.succeed({ id: "offline", name: `Offline (${e.url})` }),
249
+ NotFoundError: (e) => Effect.succeed({ id: "unknown", name: `Unknown (${e.resource})` }),
250
+ })
251
+ )
252
+
253
+ // ============================================
254
+ // 6. Run the examples
255
+ // ============================================
256
+
257
+ const program = Effect.gen(function* () {
258
+ // catchAll example
259
+ const data = yield* withFallback
260
+ yield* Effect.log(`Got data: ${data}`)
261
+
262
+ // catchTag example
263
+ const user = yield* findUserOrDefault("999")
264
+ yield* Effect.log(`Got user: ${user.name}`)
265
+
266
+ // Multiple error types
267
+ const result = yield* robustFetchUser("invalid", "999")
268
+ yield* Effect.log(`Robust result: ${result.name}`)
269
+ })
270
+
271
+ Effect.runPromise(program)
272
+ ```
273
+
274
+ **Rationale:**
275
+
276
+ Handle errors in Effect using `catchAll` to catch any error, or `catchTag` to handle specific error types.
277
+
278
+ ---
279
+
280
+
281
+ Effect makes errors explicit in your types:
282
+
283
+ 1. **Errors are typed** - You know exactly what can fail
284
+ 2. **Handle or propagate** - Can't accidentally ignore errors
285
+ 3. **Recovery options** - Provide fallbacks, retry, or transform
286
+ 4. **No try/catch** - Declarative error handling
287
+
288
+ ---
289
+
290
+ ---
291
+
292
+ ### Matching on Success and Failure with match
293
+
294
+ **Rule:** Use match to pattern match on the result of an Effect, Option, or Either, handling both success and failure cases declaratively.
295
+
296
+ **Good Example:**
297
+
298
+ ```typescript
299
+ import { Effect, Option, Either } from "effect";
300
+
301
+ // Effect: Handle both success and failure
302
+ const effect = Effect.fail("Oops!").pipe(
303
+ Effect.match({
304
+ onFailure: (err) => `Error: ${err}`,
305
+ onSuccess: (value) => `Success: ${value}`,
306
+ })
307
+ ); // Effect<string>
308
+
309
+ // Option: Handle Some and None cases
310
+ const option = Option.some(42).pipe(
311
+ Option.match({
312
+ onNone: () => "No value",
313
+ onSome: (n) => `Value: ${n}`,
314
+ })
315
+ ); // string
316
+
317
+ // Either: Handle Left and Right cases
318
+ const either = Either.left("fail").pipe(
319
+ Either.match({
320
+ onLeft: (err) => `Error: ${err}`,
321
+ onRight: (value) => `Value: ${value}`,
322
+ })
323
+ ); // string
324
+ ```
325
+
326
+ **Explanation:**
327
+
328
+ - `Effect.match` lets you handle both the error and success channels in one place.
329
+ - `Option.match` and `Either.match` let you handle all possible cases for these types, making your code exhaustive and safe.
330
+
331
+ **Anti-Pattern:**
332
+
333
+ Using nested if/else or switch statements to check for success/failure, or ignoring possible error/none/left cases, which leads to brittle and less readable code.
334
+
335
+ **Rationale:**
336
+
337
+ Use the `match` combinator to handle both success and failure cases in a single, declarative place.
338
+ This works for `Effect`, `Option`, and `Either`, and is the foundation for robust, readable error handling and branching.
339
+
340
+
341
+ Pattern matching with `match` keeps your code clear and type-safe, ensuring you handle all possible outcomes.
342
+ It avoids scattered if/else or switch statements and makes your intent explicit.
343
+
344
+ ---
345
+
346
+ ### Checking Option and Either Cases
347
+
348
+ **Rule:** Use isSome, isNone, isLeft, and isRight to check Option and Either cases for simple, type-safe conditional logic.
349
+
350
+ **Good Example:**
351
+
352
+ ```typescript
353
+ import { Option, Either } from "effect";
354
+
355
+ // Option: Check if value is Some or None
356
+ const option = Option.some(42);
357
+
358
+ if (Option.isSome(option)) {
359
+ // option.value is available here
360
+ console.log("We have a value:", option.value);
361
+ } else if (Option.isNone(option)) {
362
+ console.log("No value present");
363
+ }
364
+
365
+ // Either: Check if value is Right or Left
366
+ const either = Either.left("error");
367
+
368
+ if (Either.isRight(either)) {
369
+ // either.right is available here
370
+ console.log("Success:", either.right);
371
+ } else if (Either.isLeft(either)) {
372
+ // either.left is available here
373
+ console.log("Failure:", either.left);
374
+ }
375
+
376
+ // Filtering a collection of Options
377
+ const options = [Option.some(1), Option.none(), Option.some(3)];
378
+ const presentValues = options.filter(Option.isSome).map((o) => o.value); // [1, 3]
379
+ ```
380
+
381
+ **Explanation:**
382
+
383
+ - `Option.isSome` and `Option.isNone` let you check for presence or absence.
384
+ - `Either.isRight` and `Either.isLeft` let you check for success or failure.
385
+ - These are especially useful for filtering or quick conditional logic.
386
+
387
+ **Anti-Pattern:**
388
+
389
+ Manually checking internal tags or properties (e.g., `option._tag === "Some"`), or using unsafe type assertions, which is less safe and less readable than using the provided predicates.
390
+
391
+ **Rationale:**
392
+
393
+ Use the `isSome`, `isNone`, `isLeft`, and `isRight` predicates to check the case of an `Option` or `Either` for simple, type-safe branching.
394
+ These are useful when you need to perform quick checks or filter collections based on presence or success.
395
+
396
+
397
+ These predicates provide a concise, type-safe way to check which case you have, without resorting to manual property checks or unsafe type assertions.
398
+
399
+ ---
400
+
401
+
402
+ ## 🟡 Intermediate Patterns
403
+
404
+ ### Handle Errors with catchTag, catchTags, and catchAll
405
+
406
+ **Rule:** Handle errors with catchTag, catchTags, and catchAll.
407
+
408
+ **Good Example:**
409
+
410
+ ```typescript
411
+ import { Data, Effect } from "effect";
412
+
413
+ // Define domain types
414
+ interface User {
415
+ readonly id: string;
416
+ readonly name: string;
417
+ }
418
+
419
+ // Define specific error types
420
+ class NetworkError extends Data.TaggedError("NetworkError")<{
421
+ readonly url: string;
422
+ readonly code: number;
423
+ }> {}
424
+
425
+ class ValidationError extends Data.TaggedError("ValidationError")<{
426
+ readonly field: string;
427
+ readonly message: string;
428
+ }> {}
429
+
430
+ class NotFoundError extends Data.TaggedError("NotFoundError")<{
431
+ readonly id: string;
432
+ }> {}
433
+
434
+ // Define UserService
435
+ class UserService extends Effect.Service<UserService>()("UserService", {
436
+ sync: () => ({
437
+ // Fetch user data
438
+ fetchUser: (
439
+ id: string
440
+ ): Effect.Effect<User, NetworkError | NotFoundError> =>
441
+ Effect.gen(function* () {
442
+ yield* Effect.logInfo(`Fetching user with id: ${id}`);
443
+
444
+ if (id === "invalid") {
445
+ const url = "/api/users/" + id;
446
+ yield* Effect.logWarning(`Network error accessing: ${url}`);
447
+ return yield* Effect.fail(new NetworkError({ url, code: 500 }));
448
+ }
449
+
450
+ if (id === "missing") {
451
+ yield* Effect.logWarning(`User not found: ${id}`);
452
+ return yield* Effect.fail(new NotFoundError({ id }));
453
+ }
454
+
455
+ const user = { id, name: "John Doe" };
456
+ yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
457
+ return user;
458
+ }),
459
+
460
+ // Validate user data
461
+ validateUser: (user: User): Effect.Effect<string, ValidationError> =>
462
+ Effect.gen(function* () {
463
+ yield* Effect.logInfo(`Validating user: ${JSON.stringify(user)}`);
464
+
465
+ if (user.name.length < 3) {
466
+ yield* Effect.logWarning(
467
+ `Validation failed: name too short for user ${user.id}`
468
+ );
469
+ return yield* Effect.fail(
470
+ new ValidationError({ field: "name", message: "Name too short" })
471
+ );
472
+ }
473
+
474
+ const message = `User ${user.name} is valid`;
475
+ yield* Effect.logInfo(message);
476
+ return message;
477
+ }),
478
+ }),
479
+ }) {}
480
+
481
+ // Compose operations with error handling using catchTags
482
+ const processUser = (
483
+ userId: string
484
+ ): Effect.Effect<string, never, UserService> =>
485
+ Effect.gen(function* () {
486
+ const userService = yield* UserService;
487
+
488
+ yield* Effect.logInfo(`=== Processing user ID: ${userId} ===`);
489
+
490
+ const result = yield* userService.fetchUser(userId).pipe(
491
+ Effect.flatMap(userService.validateUser),
492
+ // Handle different error types with specific recovery logic
493
+ Effect.catchTags({
494
+ NetworkError: (e) =>
495
+ Effect.gen(function* () {
496
+ const message = `Network error: ${e.code} for ${e.url}`;
497
+ yield* Effect.logError(message);
498
+ return message;
499
+ }),
500
+ NotFoundError: (e) =>
501
+ Effect.gen(function* () {
502
+ const message = `User ${e.id} not found`;
503
+ yield* Effect.logWarning(message);
504
+ return message;
505
+ }),
506
+ ValidationError: (e) =>
507
+ Effect.gen(function* () {
508
+ const message = `Invalid ${e.field}: ${e.message}`;
509
+ yield* Effect.logWarning(message);
510
+ return message;
511
+ }),
512
+ })
513
+ );
514
+
515
+ yield* Effect.logInfo(`Result: ${result}`);
516
+ return result;
517
+ });
518
+
519
+ // Test with different scenarios
520
+ const runTests = Effect.gen(function* () {
521
+ yield* Effect.logInfo("=== Starting User Processing Tests ===");
522
+
523
+ const testCases = ["valid", "invalid", "missing"];
524
+ const results = yield* Effect.forEach(testCases, (id) => processUser(id));
525
+
526
+ yield* Effect.logInfo("=== User Processing Tests Complete ===");
527
+ return results;
528
+ });
529
+
530
+ // Run the program
531
+ Effect.runPromise(Effect.provide(runTests, UserService.Default));
532
+ ```
533
+
534
+ **Explanation:**
535
+ Use `catchTag` to handle specific error types in a type-safe, composable way.
536
+
537
+ **Anti-Pattern:**
538
+
539
+ Using `try/catch` blocks inside your Effect compositions. It breaks the
540
+ declarative flow and bypasses Effect's powerful, type-safe error channels.
541
+
542
+ **Rationale:**
543
+
544
+ To recover from failures, use the `catch*` family of functions.
545
+ `Effect.catchTag` for specific tagged errors, `Effect.catchTags` for multiple,
546
+ and `Effect.catchAll` for any error.
547
+
548
+
549
+ Effect's structured error handling allows you to build resilient applications.
550
+ By using tagged errors and `catchTag`, you can handle different failure
551
+ scenarios with different logic in a type-safe way.
552
+
553
+ ---
554
+
555
+ ### Mapping Errors to Fit Your Domain
556
+
557
+ **Rule:** Use Effect.mapError to transform errors and create clean architectural boundaries between layers.
558
+
559
+ **Good Example:**
560
+
561
+ A `UserRepository` uses a `Database` service. The `Database` can fail with specific errors, but the `UserRepository` maps them to a single, generic `RepositoryError` before they are exposed to the rest of the application.
562
+
563
+ ```typescript
564
+ import { Effect, Data } from "effect";
565
+
566
+ // Low-level, specific errors from the database layer
567
+ class ConnectionError extends Data.TaggedError("ConnectionError") {}
568
+ class QueryError extends Data.TaggedError("QueryError") {}
569
+
570
+ // A generic error for the repository layer
571
+ class RepositoryError extends Data.TaggedError("RepositoryError")<{
572
+ readonly cause: unknown;
573
+ }> {}
574
+
575
+ // The inner service
576
+ const dbQuery = (): Effect.Effect<
577
+ { name: string },
578
+ ConnectionError | QueryError
579
+ > => Effect.fail(new ConnectionError());
580
+
581
+ // The outer service uses `mapError` to create a clean boundary.
582
+ // Its public signature only exposes `RepositoryError`.
583
+ const findUser = (): Effect.Effect<{ name: string }, RepositoryError> =>
584
+ dbQuery().pipe(
585
+ Effect.mapError((error) => new RepositoryError({ cause: error }))
586
+ );
587
+
588
+ // Demonstrate the error mapping
589
+ const program = Effect.gen(function* () {
590
+ yield* Effect.logInfo("Attempting to find user...");
591
+
592
+ try {
593
+ const user = yield* findUser();
594
+ yield* Effect.logInfo(`Found user: ${user.name}`);
595
+ } catch (error) {
596
+ yield* Effect.logInfo("This won't be reached due to Effect error handling");
597
+ }
598
+ }).pipe(
599
+ Effect.catchAll((error) =>
600
+ Effect.gen(function* () {
601
+ if (error instanceof RepositoryError) {
602
+ yield* Effect.logInfo(`Repository error occurred: ${error._tag}`);
603
+ if (
604
+ error.cause instanceof ConnectionError ||
605
+ error.cause instanceof QueryError
606
+ ) {
607
+ yield* Effect.logInfo(`Original cause: ${error.cause._tag}`);
608
+ }
609
+ } else {
610
+ yield* Effect.logInfo(`Unexpected error: ${error}`);
611
+ }
612
+ })
613
+ )
614
+ );
615
+
616
+ Effect.runPromise(program);
617
+ ```
618
+
619
+ ---
620
+
621
+ **Anti-Pattern:**
622
+
623
+ Allowing low-level, implementation-specific errors to "leak" out of a service's public API. This creates tight coupling between layers.
624
+
625
+ ```typescript
626
+ import { Effect } from "effect";
627
+ import { ConnectionError, QueryError } from "./somewhere"; // From previous example
628
+
629
+ // ❌ WRONG: This function's error channel is "leaky".
630
+ // It exposes the internal implementation details of the database.
631
+ const findUserUnsafely = (): Effect.Effect<
632
+ { name: string },
633
+ ConnectionError | QueryError // <-- Leaky abstraction
634
+ > => {
635
+ // ... logic that calls the database
636
+ return Effect.fail(new ConnectionError());
637
+ };
638
+
639
+ // Now, any code that calls `findUserUnsafely` has to know about and handle
640
+ // both `ConnectionError` and `QueryError`. If we change the database,
641
+ // all of that calling code might have to change too.
642
+ ```
643
+
644
+ **Rationale:**
645
+
646
+ When an inner service can fail with specific errors, use `Effect.mapError` in the outer service to catch those specific errors and transform them into a more general error suitable for its own domain.
647
+
648
+ ---
649
+
650
+
651
+ This pattern is essential for creating clean architectural boundaries and preventing "leaky abstractions." An outer layer of your application (e.g., a `UserService`) should not expose the internal failure details of the layers it depends on (e.g., a `Database` that can fail with `ConnectionError` or `QueryError`).
652
+
653
+ By using `Effect.mapError`, the outer layer can define its own, more abstract error type (like `RepositoryError`) and map all the specific, low-level errors into it. This decouples the layers. If you later swap your database implementation, you only need to update the mapping logic within the repository layer; none of the code that _uses_ the repository needs to change.
654
+
655
+ ---
656
+
657
+ ---
658
+
659
+ ### Control Repetition with Schedule
660
+
661
+ **Rule:** Use Schedule to create composable policies for controlling the repetition and retrying of effects.
662
+
663
+ **Good Example:**
664
+
665
+ This example demonstrates composition by creating a common, robust retry policy: exponential backoff with jitter, limited to 5 attempts.
666
+
667
+ ```typescript
668
+ import { Effect, Schedule, Duration } from "effect";
669
+
670
+ // A simple effect that can fail
671
+ const flakyEffect = Effect.try({
672
+ try: () => {
673
+ if (Math.random() > 0.2) {
674
+ throw new Error("Transient error");
675
+ }
676
+ return "Operation succeeded!";
677
+ },
678
+ catch: (error: unknown) => {
679
+ Effect.logInfo("Operation failed, retrying...");
680
+ return error;
681
+ },
682
+ });
683
+
684
+ // --- Building a Composable Schedule ---
685
+
686
+ // 1. Start with a base exponential backoff (100ms, 200ms, 400ms...)
687
+ const exponentialBackoff = Schedule.exponential("100 millis");
688
+
689
+ // 2. Add random jitter to avoid thundering herd problems
690
+ const withJitter = Schedule.jittered(exponentialBackoff);
691
+
692
+ // 3. Limit the schedule to a maximum of 5 repetitions
693
+ const limitedWithJitter = Schedule.compose(withJitter, Schedule.recurs(5));
694
+
695
+ // --- Using the Schedule ---
696
+ const program = Effect.gen(function* () {
697
+ yield* Effect.logInfo("Starting operation...");
698
+ const result = yield* Effect.retry(flakyEffect, limitedWithJitter);
699
+ yield* Effect.logInfo(`Final result: ${result}`);
700
+ });
701
+
702
+ // Run the program
703
+ Effect.runPromise(program);
704
+ ```
705
+
706
+ ---
707
+
708
+ **Anti-Pattern:**
709
+
710
+ Writing manual, imperative retry logic. This is verbose, stateful, hard to reason about, and not easily composable.
711
+
712
+ ```typescript
713
+ import { Effect } from "effect";
714
+ import { flakyEffect } from "./somewhere";
715
+
716
+ // ❌ WRONG: Manual, stateful, and complex retry logic.
717
+ function manualRetry(
718
+ effect: typeof flakyEffect,
719
+ retriesLeft: number,
720
+ delay: number
721
+ ): Effect.Effect<string, "ApiError"> {
722
+ return effect.pipe(
723
+ Effect.catchTag("ApiError", () => {
724
+ if (retriesLeft > 0) {
725
+ return Effect.sleep(delay).pipe(
726
+ Effect.flatMap(() => manualRetry(effect, retriesLeft - 1, delay * 2))
727
+ );
728
+ }
729
+ return Effect.fail("ApiError" as const);
730
+ })
731
+ );
732
+ }
733
+
734
+ const program = manualRetry(flakyEffect, 5, 100);
735
+ ```
736
+
737
+ **Rationale:**
738
+
739
+ A `Schedule<In, Out>` is a highly-composable blueprint that defines a recurring schedule. It takes an input of type `In` (e.g., the error from a failed effect) and produces an output of type `Out` (e.g., the decision to continue). Use `Schedule` with operators like `Effect.repeat` and `Effect.retry` to control complex repeating logic.
740
+
741
+ ---
742
+
743
+
744
+ While you could write manual loops or recursive functions, `Schedule` provides a much more powerful, declarative, and composable way to manage repetition. The key benefits are:
745
+
746
+ - **Declarative:** You separate the _what_ (the effect to run) from the _how_ and _when_ (the schedule it runs on).
747
+ - **Composable:** You can build complex schedules from simple, primitive ones. For example, you can create a schedule that runs "up to 5 times, with an exponential backoff, plus some random jitter" by composing `Schedule.recurs`, `Schedule.exponential`, and `Schedule.jittered`.
748
+ - **Stateful:** A `Schedule` keeps track of its own state (like the number of repetitions), making it easy to create policies that depend on the execution history.
749
+
750
+ ---
751
+
752
+ ---
753
+
754
+ ### Leverage Effect's Built-in Structured Logging
755
+
756
+ **Rule:** Leverage Effect's built-in structured logging.
757
+
758
+ **Good Example:**
759
+
760
+ ```typescript
761
+ import { Effect } from "effect";
762
+
763
+ const program = Effect.logDebug("Processing user", { userId: 123 });
764
+
765
+ // Run the program with debug logging enabled
766
+ Effect.runSync(
767
+ program.pipe(Effect.tap(() => Effect.log("Debug logging enabled")))
768
+ );
769
+ ```
770
+
771
+ **Explanation:**
772
+ Using Effect's logging system ensures your logs are structured, filterable,
773
+ and context-aware.
774
+
775
+ **Anti-Pattern:**
776
+
777
+ Calling `console.log` directly within an Effect composition. This is an
778
+ unmanaged side-effect that bypasses all the benefits of Effect's logging system.
779
+
780
+ **Rationale:**
781
+
782
+ Use the built-in `Effect.log*` family of functions for all application logging
783
+ instead of using `console.log`.
784
+
785
+
786
+ Effect's logger is structured, context-aware (with trace IDs), configurable
787
+ via `Layer`, and testable. It's a first-class citizen, not an unmanaged
788
+ side-effect.
789
+
790
+ ---
791
+
792
+ ### Matching Tagged Unions with matchTag and matchTags
793
+
794
+ **Rule:** Use matchTag and matchTags to handle specific cases of tagged unions or custom error types in a declarative, type-safe way.
795
+
796
+ **Good Example:**
797
+
798
+ ```typescript
799
+ import { Data, Effect } from "effect";
800
+
801
+ // Define a tagged error type
802
+ class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {}
803
+ class ValidationError extends Data.TaggedError("ValidationError")<{
804
+ message: string;
805
+ }> {}
806
+
807
+ type MyError = NotFoundError | ValidationError;
808
+
809
+ // Effect: Match on specific error tags
810
+ const effect: Effect.Effect<string, never, never> = Effect.fail(
811
+ new ValidationError({ message: "Invalid input" }) as MyError
812
+ ).pipe(
813
+ Effect.catchTags({
814
+ NotFoundError: () => Effect.succeed("Not found!"),
815
+ ValidationError: (err) =>
816
+ Effect.succeed(`Validation failed: ${err.message}`),
817
+ })
818
+ ); // Effect<string>
819
+ ```
820
+
821
+ **Explanation:**
822
+
823
+ - `matchTag` lets you branch on the specific tag of a tagged union or custom error type.
824
+ - This is safer and more maintainable than using `instanceof` or manual property checks.
825
+
826
+ **Anti-Pattern:**
827
+
828
+ Using `instanceof`, manual property checks, or switch statements to distinguish between cases, which is error-prone and less type-safe than declarative pattern matching.
829
+
830
+ **Rationale:**
831
+
832
+ Use the `matchTag` and `matchTags` combinators to pattern match on specific cases of tagged unions or custom error types.
833
+ This enables precise, type-safe branching and is especially useful for handling domain-specific errors or ADTs.
834
+
835
+
836
+ Tagged unions (a.k.a. algebraic data types or ADTs) are a powerful way to model domain logic.
837
+ Pattern matching on tags lets you handle each case explicitly, making your code robust, maintainable, and exhaustive.
838
+
839
+ ---
840
+
841
+ ### Conditionally Branching Workflows
842
+
843
+ **Rule:** Use predicate-based operators like Effect.filter and Effect.if to declaratively control workflow branching.
844
+
845
+ **Good Example:**
846
+
847
+ Here, we use `Effect.filterOrFail` with named predicates to validate a user before proceeding. The intent is crystal clear, and the business rules (`isActive`, `isAdmin`) are reusable.
848
+
849
+ ```typescript
850
+ import { Effect } from "effect";
851
+
852
+ interface User {
853
+ id: number;
854
+ status: "active" | "inactive";
855
+ roles: string[];
856
+ }
857
+
858
+ type UserError = "DbError" | "UserIsInactive" | "UserIsNotAdmin";
859
+
860
+ const findUser = (id: number): Effect.Effect<User, "DbError"> =>
861
+ Effect.succeed({ id, status: "active", roles: ["admin"] });
862
+
863
+ // Reusable, testable predicates that document business rules.
864
+ const isActive = (user: User): boolean => user.status === "active";
865
+
866
+ const isAdmin = (user: User): boolean => user.roles.includes("admin");
867
+
868
+ const program = (id: number): Effect.Effect<string, UserError> =>
869
+ findUser(id).pipe(
870
+ // Validate user is active using Effect.filterOrFail
871
+ Effect.filterOrFail(isActive, () => "UserIsInactive" as const),
872
+ // Validate user is admin using Effect.filterOrFail
873
+ Effect.filterOrFail(isAdmin, () => "UserIsNotAdmin" as const),
874
+ // Success case
875
+ Effect.map((user) => `Welcome, admin user #${user.id}!​`)
876
+ );
877
+
878
+ // We can then handle the specific failures in a type-safe way.
879
+ const handled = program(123).pipe(
880
+ Effect.match({
881
+ onFailure: (error) => {
882
+ switch (error) {
883
+ case "UserIsNotAdmin":
884
+ return "Access denied: requires admin role.";
885
+ case "UserIsInactive":
886
+ return "Access denied: user is not active.";
887
+ case "DbError":
888
+ return "Error: could not find user.";
889
+ default:
890
+ return `Unknown error: ${error}`;
891
+ }
892
+ },
893
+ onSuccess: (result) => result,
894
+ })
895
+ );
896
+
897
+ // Run the program
898
+ const programWithLogging = Effect.gen(function* () {
899
+ const result = yield* handled;
900
+ yield* Effect.log(result);
901
+ return result;
902
+ });
903
+
904
+ Effect.runPromise(programWithLogging);
905
+ ```
906
+
907
+ ---
908
+
909
+ **Anti-Pattern:**
910
+
911
+ Using `Effect.flatMap` with a manual `if` statement and forgetting to handle the `else` case. This is a common mistake that leads to an inferred type of `Effect<void, ...>`, which can cause confusing type errors downstream because the success value is lost.
912
+
913
+ ```typescript
914
+ import { Effect } from "effect";
915
+ import { findUser, isAdmin } from "./somewhere"; // From previous example
916
+
917
+ // ❌ WRONG: The `else` case is missing.
918
+ const program = (id: number) =>
919
+ findUser(id).pipe(
920
+ Effect.flatMap((user) => {
921
+ if (isAdmin(user)) {
922
+ // This returns Effect<User>, but what happens if the user is not an admin?
923
+ return Effect.succeed(user);
924
+ }
925
+ // Because there's no `else` branch, TypeScript infers that this
926
+ // block can also implicitly return `void`.
927
+ // The resulting type is Effect<User | void, "DbError">, which is problematic.
928
+ }),
929
+ // This `map` will now have a type error because `u` could be `void`.
930
+ Effect.map((u) => `Welcome, ${u.name}!​`)
931
+ );
932
+
933
+ // `Effect.filterOrFail` avoids this problem entirely by forcing a failure,
934
+ // which keeps the success channel clean and correctly typed.
935
+ ```
936
+
937
+ ### Why This is Better
938
+
939
+ - **It's a Real Bug:** This isn't just a style issue; it's a legitimate logical error that leads to incorrect types and broken code.
940
+ - **It's a Common Mistake:** Developers new to functional pipelines often forget that every path must return a value.
941
+ - **It Reinforces the "Why":** It perfectly demonstrates _why_ `Effect.filterOrFail` is superior: `filterOrFail` guarantees that if the condition fails, the computation fails, preserving the integrity of the success channel.
942
+
943
+ **Rationale:**
944
+
945
+ To make decisions based on a successful value within an `Effect` pipeline, use predicate-based operators:
946
+
947
+ - **To Validate and Fail:** Use `Effect.filterOrFail(predicate, onFailure)` to stop the workflow if a condition is not met.
948
+ - **To Choose a Path:** Use `Effect.if(condition, { onTrue, onFalse })` or `Effect.gen` to execute different effects based on a condition.
949
+
950
+ ---
951
+
952
+
953
+ This pattern allows you to embed decision-making logic directly into your composition pipelines, making your code more declarative and readable. It solves two key problems:
954
+
955
+ 1. **Separation of Concerns:** It cleanly separates the logic of producing a value from the logic of validating or making decisions about that value.
956
+ 2. **Reusable Business Logic:** A predicate function (e.g., `const isAdmin = (user: User) => ...`) becomes a named, reusable, and testable piece of business logic, far superior to scattering inline `if` statements throughout your code.
957
+
958
+ Using these operators turns conditional logic into a composable part of your `Effect`, rather than an imperative statement that breaks the flow.
959
+
960
+ ---
961
+
962
+ ---
963
+
964
+ ### Effectful Pattern Matching with matchEffect
965
+
966
+ **Rule:** Use matchEffect to pattern match on the result of an Effect, running effectful logic for both success and failure cases.
967
+
968
+ **Good Example:**
969
+
970
+ ```typescript
971
+ import { Effect } from "effect";
972
+
973
+ // Effect: Run different Effects on success or failure
974
+ const effect = Effect.fail("Oops!").pipe(
975
+ Effect.matchEffect({
976
+ onFailure: (err) => Effect.logError(`Error: ${err}`),
977
+ onSuccess: (value) => Effect.log(`Success: ${value}`),
978
+ })
979
+ ); // Effect<void>
980
+ ```
981
+
982
+ **Explanation:**
983
+
984
+ - `matchEffect` allows you to run an Effect for both the success and failure cases.
985
+ - This is useful for logging, cleanup, retries, or any effectful side effect that depends on the outcome.
986
+
987
+ **Anti-Pattern:**
988
+
989
+ Using `match` to return values and then wrapping them in Effects, or duplicating logic for side effects, instead of using `matchEffect` for direct effectful branching.
990
+
991
+ **Rationale:**
992
+
993
+ Use the `matchEffect` combinator to perform effectful branching based on whether an Effect succeeds or fails.
994
+ This allows you to run different Effects for each case, enabling rich, composable workflows.
995
+
996
+
997
+ Sometimes, handling a success or failure requires running additional Effects (e.g., logging, retries, cleanup).
998
+ `matchEffect` lets you do this declaratively, keeping your code composable and type-safe.
999
+
1000
+ ---
1001
+
1002
+ ### Retry Operations Based on Specific Errors
1003
+
1004
+ **Rule:** Use predicate-based retry policies to retry an operation only for specific, recoverable errors.
1005
+
1006
+ **Good Example:**
1007
+
1008
+ This example simulates an API client that can fail with different, specific error types. The retry policy is configured to _only_ retry on `ServerBusyError` and give up immediately on `NotFoundError`.
1009
+
1010
+ ```typescript
1011
+ import { Data, Effect, Schedule } from "effect";
1012
+
1013
+ // Define specific, tagged errors for our API client
1014
+ class ServerBusyError extends Data.TaggedError("ServerBusyError") {}
1015
+ class NotFoundError extends Data.TaggedError("NotFoundError") {}
1016
+
1017
+ let attemptCount = 0;
1018
+
1019
+ // A flaky API call that can fail in different ways
1020
+ const flakyApiCall = Effect.try({
1021
+ try: () => {
1022
+ attemptCount++;
1023
+ const random = Math.random();
1024
+
1025
+ if (attemptCount <= 2) {
1026
+ // First two attempts fail with ServerBusyError (retryable)
1027
+ console.log(
1028
+ `Attempt ${attemptCount}: API call failed - Server is busy. Retrying...`
1029
+ );
1030
+ throw new ServerBusyError();
1031
+ }
1032
+
1033
+ // Third attempt succeeds
1034
+ console.log(`Attempt ${attemptCount}: API call succeeded!​`);
1035
+ return { data: "success", attempt: attemptCount };
1036
+ },
1037
+ catch: (e) => e as ServerBusyError | NotFoundError,
1038
+ });
1039
+
1040
+ // A predicate that returns true only for the error we want to retry
1041
+ const isRetryableError = (e: ServerBusyError | NotFoundError) =>
1042
+ e._tag === "ServerBusyError";
1043
+
1044
+ // A policy that retries 3 times, but only if the error is retryable
1045
+ const selectiveRetryPolicy = Schedule.recurs(3).pipe(
1046
+ Schedule.whileInput(isRetryableError),
1047
+ Schedule.addDelay(() => "100 millis")
1048
+ );
1049
+
1050
+ const program = Effect.gen(function* () {
1051
+ yield* Effect.logInfo("=== Retry Based on Specific Errors Demo ===");
1052
+
1053
+ try {
1054
+ const result = yield* flakyApiCall.pipe(Effect.retry(selectiveRetryPolicy));
1055
+ yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);
1056
+ return result;
1057
+ } catch (error) {
1058
+ yield* Effect.logInfo("This won't be reached due to Effect error handling");
1059
+ return null;
1060
+ }
1061
+ }).pipe(
1062
+ Effect.catchAll((error) =>
1063
+ Effect.gen(function* () {
1064
+ if (error instanceof NotFoundError) {
1065
+ yield* Effect.logInfo("Failed with NotFoundError - not retrying");
1066
+ } else if (error instanceof ServerBusyError) {
1067
+ yield* Effect.logInfo("Failed with ServerBusyError after all retries");
1068
+ } else {
1069
+ yield* Effect.logInfo(`Failed with unexpected error: ${error}`);
1070
+ }
1071
+ return null;
1072
+ })
1073
+ )
1074
+ );
1075
+
1076
+ // Also demonstrate a case where NotFoundError is not retried
1077
+ const demonstrateNotFound = Effect.gen(function* () {
1078
+ yield* Effect.logInfo("\n=== Demonstrating Non-Retryable Error ===");
1079
+
1080
+ const alwaysNotFound = Effect.fail(new NotFoundError());
1081
+
1082
+ const result = yield* alwaysNotFound.pipe(
1083
+ Effect.retry(selectiveRetryPolicy),
1084
+ Effect.catchAll((error) =>
1085
+ Effect.gen(function* () {
1086
+ yield* Effect.logInfo(`NotFoundError was not retried: ${error._tag}`);
1087
+ return null;
1088
+ })
1089
+ )
1090
+ );
1091
+
1092
+ return result;
1093
+ });
1094
+
1095
+ Effect.runPromise(program.pipe(Effect.flatMap(() => demonstrateNotFound)));
1096
+ ```
1097
+
1098
+ ---
1099
+
1100
+ **Anti-Pattern:**
1101
+
1102
+ Using a generic `Effect.retry` that retries on all errors. This can lead to wasted resources and obscure permanent issues.
1103
+
1104
+ ```typescript
1105
+ import { Effect, Schedule } from "effect";
1106
+ import { flakyApiCall } from "./somewhere"; // From previous example
1107
+
1108
+ // ❌ WRONG: This policy will retry even if the API returns a 404 Not Found.
1109
+ // This wastes time and network requests on an error that will never succeed.
1110
+ const blindRetryPolicy = Schedule.recurs(3);
1111
+
1112
+ const program = flakyApiCall.pipe(Effect.retry(blindRetryPolicy));
1113
+ ```
1114
+
1115
+ **Rationale:**
1116
+
1117
+ To selectively retry an operation, use `Effect.retry` with a `Schedule` that includes a predicate. The most common way is to use `Schedule.whileInput((error) => ...)`, which will continue retrying only as long as the predicate returns `true` for the error that occurred.
1118
+
1119
+ ---
1120
+
1121
+
1122
+ Not all errors are created equal. Retrying on a permanent error like "permission denied" or "not found" is pointless and can hide underlying issues. You only want to retry on _transient_, recoverable errors, such as network timeouts or "server busy" responses.
1123
+
1124
+ By adding a predicate to your retry schedule, you gain fine-grained control over the retry logic. This allows you to build much more intelligent and efficient error handling systems that react appropriately to different failure modes. This is a common requirement for building robust clients for external APIs.
1125
+
1126
+ ---
1127
+
1128
+ ---
1129
+
1130
+ ### Handling Specific Errors with catchTag and catchTags
1131
+
1132
+ **Rule:** Use catchTag and catchTags to handle specific tagged error types in the Effect failure channel, providing targeted recovery logic.
1133
+
1134
+ **Good Example:**
1135
+
1136
+ ```typescript
1137
+ import { Effect, Data } from "effect";
1138
+
1139
+ // Define tagged error types
1140
+ class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {}
1141
+ class ValidationError extends Data.TaggedError("ValidationError")<{
1142
+ message: string;
1143
+ }> {}
1144
+
1145
+ type MyError = NotFoundError | ValidationError;
1146
+
1147
+ // Effect: Handle only ValidationError, let others propagate
1148
+ const effect = Effect.fail(
1149
+ new ValidationError({ message: "Invalid input" }) as MyError
1150
+ ).pipe(
1151
+ Effect.catchTag("ValidationError", (err) =>
1152
+ Effect.succeed(`Recovered from validation error: ${err.message}`)
1153
+ )
1154
+ ); // Effect<string>
1155
+
1156
+ // Effect: Handle multiple error tags
1157
+ const effect2 = Effect.fail(new NotFoundError() as MyError).pipe(
1158
+ Effect.catchTags({
1159
+ NotFoundError: () => Effect.succeed("Handled not found!"),
1160
+ ValidationError: (err) =>
1161
+ Effect.succeed(`Handled validation: ${err.message}`),
1162
+ })
1163
+ ); // Effect<string>
1164
+ ```
1165
+
1166
+ **Explanation:**
1167
+
1168
+ - `catchTag` lets you recover from a specific tagged error type.
1169
+ - `catchTags` lets you handle multiple tagged error types in one place.
1170
+ - Unhandled errors continue to propagate, preserving error safety.
1171
+
1172
+ **Anti-Pattern:**
1173
+
1174
+ Catching all errors generically (e.g., with `catchAll`) and using manual type checks or property inspection, which is less safe and more error-prone than using tagged error combinators.
1175
+
1176
+ **Rationale:**
1177
+
1178
+ Use the `catchTag` and `catchTags` combinators to recover from or handle specific tagged error types in the Effect failure channel.
1179
+ This enables precise, type-safe error recovery and is especially useful for domain-specific error handling.
1180
+
1181
+
1182
+ Not all errors should be handled the same way.
1183
+ By matching on specific error tags, you can provide targeted recovery logic for each error type, while letting unhandled errors propagate as needed.
1184
+
1185
+ ---
1186
+
1187
+ ### Handle Flaky Operations with Retries and Timeouts
1188
+
1189
+ **Rule:** Use Effect.retry and Effect.timeout to build resilience against slow or intermittently failing effects.
1190
+
1191
+ **Good Example:**
1192
+
1193
+ This program attempts to fetch data from a flaky API. It will retry the request up to 3 times with increasing delays if it fails. It will also give up entirely if any single attempt takes longer than 2 seconds.
1194
+
1195
+ ```typescript
1196
+ import { Data, Duration, Effect, Schedule } from "effect";
1197
+
1198
+ // Define domain types
1199
+ interface ApiResponse {
1200
+ readonly data: string;
1201
+ }
1202
+
1203
+ // Define error types
1204
+ class ApiError extends Data.TaggedError("ApiError")<{
1205
+ readonly message: string;
1206
+ readonly attempt: number;
1207
+ }> {}
1208
+
1209
+ class TimeoutError extends Data.TaggedError("TimeoutError")<{
1210
+ readonly duration: string;
1211
+ readonly attempt: number;
1212
+ }> {}
1213
+
1214
+ // Define API service
1215
+ class ApiService extends Effect.Service<ApiService>()("ApiService", {
1216
+ sync: () => ({
1217
+ // Flaky API call that might fail or be slow
1218
+ fetchData: (): Effect.Effect<ApiResponse, ApiError | TimeoutError> =>
1219
+ Effect.gen(function* () {
1220
+ const attempt = Math.floor(Math.random() * 5) + 1;
1221
+ yield* Effect.logInfo(`Attempt ${attempt}: Making API call...`);
1222
+
1223
+ if (Math.random() > 0.3) {
1224
+ yield* Effect.logWarning(`Attempt ${attempt}: API call failed`);
1225
+ return yield* Effect.fail(
1226
+ new ApiError({
1227
+ message: "API Error",
1228
+ attempt,
1229
+ })
1230
+ );
1231
+ }
1232
+
1233
+ const delay = Math.random() * 3000;
1234
+ yield* Effect.logInfo(
1235
+ `Attempt ${attempt}: API call will take ${delay.toFixed(0)}ms`
1236
+ );
1237
+
1238
+ yield* Effect.sleep(Duration.millis(delay));
1239
+
1240
+ const response = { data: "some important data" };
1241
+ yield* Effect.logInfo(
1242
+ `Attempt ${attempt}: API call succeeded with data: ${JSON.stringify(response)}`
1243
+ );
1244
+ return response;
1245
+ }),
1246
+ }),
1247
+ }) {}
1248
+
1249
+ // Define retry policy: exponential backoff, up to 3 retries
1250
+ const retryPolicy = Schedule.exponential(Duration.millis(100)).pipe(
1251
+ Schedule.compose(Schedule.recurs(3)),
1252
+ Schedule.tapInput((error: ApiError | TimeoutError) =>
1253
+ Effect.logWarning(
1254
+ `Retrying after error: ${error._tag} (Attempt ${error.attempt})`
1255
+ )
1256
+ )
1257
+ );
1258
+
1259
+ // Create program with proper error handling
1260
+ const program = Effect.gen(function* () {
1261
+ const api = yield* ApiService;
1262
+
1263
+ yield* Effect.logInfo("=== Starting API calls with retry and timeout ===");
1264
+
1265
+ // Make multiple test calls
1266
+ for (let i = 1; i <= 3; i++) {
1267
+ yield* Effect.logInfo(`\n--- Test Call ${i} ---`);
1268
+
1269
+ const result = yield* api.fetchData().pipe(
1270
+ Effect.timeout(Duration.seconds(2)),
1271
+ Effect.catchTag("TimeoutException", () =>
1272
+ Effect.fail(new TimeoutError({ duration: "2 seconds", attempt: i }))
1273
+ ),
1274
+ Effect.retry(retryPolicy),
1275
+ Effect.catchTags({
1276
+ ApiError: (error) =>
1277
+ Effect.gen(function* () {
1278
+ yield* Effect.logError(
1279
+ `All retries failed: ${error.message} (Last attempt: ${error.attempt})`
1280
+ );
1281
+ return { data: "fallback data due to API error" } as ApiResponse;
1282
+ }),
1283
+ TimeoutError: (error) =>
1284
+ Effect.gen(function* () {
1285
+ yield* Effect.logError(
1286
+ `All retries timed out after ${error.duration} (Last attempt: ${error.attempt})`
1287
+ );
1288
+ return { data: "fallback data due to timeout" } as ApiResponse;
1289
+ }),
1290
+ })
1291
+ );
1292
+
1293
+ yield* Effect.logInfo(`Result: ${JSON.stringify(result)}`);
1294
+ }
1295
+
1296
+ yield* Effect.logInfo("\n=== API calls complete ===");
1297
+ });
1298
+
1299
+ // Run the program
1300
+ Effect.runPromise(Effect.provide(program, ApiService.Default));
1301
+ ```
1302
+
1303
+ ---
1304
+
1305
+ **Anti-Pattern:**
1306
+
1307
+ Writing manual retry and timeout logic. This is verbose, complex, and easy to get wrong. It clutters your business logic with concerns that Effect can handle declaratively.
1308
+
1309
+ ```typescript
1310
+ // ❌ WRONG: Manual, complex, and error-prone logic.
1311
+ async function manualRetryAndTimeout() {
1312
+ for (let i = 0; i < 3; i++) {
1313
+ try {
1314
+ const controller = new AbortController();
1315
+ const timeoutId = setTimeout(() => controller.abort(), 2000);
1316
+
1317
+ const response = await fetch("...", { signal: controller.signal });
1318
+ clearTimeout(timeoutId);
1319
+
1320
+ return await response.json();
1321
+ } catch (error) {
1322
+ if (i === 2) throw error; // Last attempt, re-throw
1323
+ await new Promise((res) => setTimeout(res, 100 * 2 ** i)); // Manual backoff
1324
+ }
1325
+ }
1326
+ }
1327
+ ```
1328
+
1329
+ **Rationale:**
1330
+
1331
+ To build robust applications that can withstand unreliable external systems, apply two key operators to your effects:
1332
+
1333
+ - **`Effect.retry(policy)`**: To automatically re-run a failing effect according to a schedule.
1334
+ - **`Effect.timeout(duration)`**: To interrupt an effect that takes too long to complete.
1335
+
1336
+ ---
1337
+
1338
+
1339
+ In distributed systems, failure is normal. APIs can fail intermittently, and network latency can spike. Hard-coding your application to try an operation only once makes it brittle.
1340
+
1341
+ - **Retries:** The `Effect.retry` operator, combined with a `Schedule` policy, provides a powerful, declarative way to handle transient failures. Instead of writing complex `try/catch` loops, you can simply define a policy like "retry 3 times, with an exponential backoff delay between attempts."
1342
+
1343
+ - **Timeouts:** An operation might not fail, but instead hang indefinitely. `Effect.timeout` prevents this by racing your effect against a timer. If your effect doesn't complete within the specified duration, it is automatically interrupted, preventing your application from getting stuck.
1344
+
1345
+ Combining these two patterns is a best practice for any interaction with an external service.
1346
+
1347
+ ---
1348
+
1349
+ ---
1350
+
1351
+
1352
+ ## 🟠 Advanced Patterns
1353
+
1354
+ ### Handle Unexpected Errors by Inspecting the Cause
1355
+
1356
+ **Rule:** Handle unexpected errors by inspecting the cause.
1357
+
1358
+ **Good Example:**
1359
+
1360
+ ```typescript
1361
+ import { Cause, Effect, Data, Schedule, Duration } from "effect";
1362
+
1363
+ // Define domain types
1364
+ interface DatabaseConfig {
1365
+ readonly url: string;
1366
+ }
1367
+
1368
+ interface DatabaseConnection {
1369
+ readonly success: true;
1370
+ }
1371
+
1372
+ interface UserData {
1373
+ readonly id: string;
1374
+ readonly name: string;
1375
+ }
1376
+
1377
+ // Define error types
1378
+ class DatabaseError extends Data.TaggedError("DatabaseError")<{
1379
+ readonly operation: string;
1380
+ readonly details: string;
1381
+ }> {}
1382
+
1383
+ class ValidationError extends Data.TaggedError("ValidationError")<{
1384
+ readonly field: string;
1385
+ readonly message: string;
1386
+ }> {}
1387
+
1388
+ // Define database service
1389
+ class DatabaseService extends Effect.Service<DatabaseService>()(
1390
+ "DatabaseService",
1391
+ {
1392
+ sync: () => ({
1393
+ // Connect to database with proper error handling
1394
+ connect: (
1395
+ config: DatabaseConfig
1396
+ ): Effect.Effect<DatabaseConnection, DatabaseError> =>
1397
+ Effect.gen(function* () {
1398
+ yield* Effect.logInfo(`Connecting to database: ${config.url}`);
1399
+
1400
+ if (!config.url) {
1401
+ const error = new DatabaseError({
1402
+ operation: "connect",
1403
+ details: "Missing URL",
1404
+ });
1405
+ yield* Effect.logError(`Database error: ${JSON.stringify(error)}`);
1406
+ return yield* Effect.fail(error);
1407
+ }
1408
+
1409
+ // Simulate unexpected errors
1410
+ if (config.url === "invalid") {
1411
+ yield* Effect.logError("Invalid connection string");
1412
+ return yield* Effect.sync(() => {
1413
+ throw new Error("Failed to parse connection string");
1414
+ });
1415
+ }
1416
+
1417
+ if (config.url === "timeout") {
1418
+ yield* Effect.logError("Connection timeout");
1419
+ return yield* Effect.sync(() => {
1420
+ throw new Error("Connection timed out");
1421
+ });
1422
+ }
1423
+
1424
+ yield* Effect.logInfo("Database connection successful");
1425
+ return { success: true };
1426
+ }),
1427
+ }),
1428
+ }
1429
+ ) {}
1430
+
1431
+ // Define user service
1432
+ class UserService extends Effect.Service<UserService>()("UserService", {
1433
+ sync: () => ({
1434
+ // Parse user data with validation
1435
+ parseUser: (input: unknown): Effect.Effect<UserData, ValidationError> =>
1436
+ Effect.gen(function* () {
1437
+ yield* Effect.logInfo(`Parsing user data: ${JSON.stringify(input)}`);
1438
+
1439
+ try {
1440
+ if (typeof input !== "object" || !input) {
1441
+ const error = new ValidationError({
1442
+ field: "input",
1443
+ message: "Invalid input type",
1444
+ });
1445
+ yield* Effect.logWarning(
1446
+ `Validation error: ${JSON.stringify(error)}`
1447
+ );
1448
+ throw error;
1449
+ }
1450
+
1451
+ const data = input as Record<string, unknown>;
1452
+
1453
+ if (typeof data.id !== "string" || typeof data.name !== "string") {
1454
+ const error = new ValidationError({
1455
+ field: "input",
1456
+ message: "Missing required fields",
1457
+ });
1458
+ yield* Effect.logWarning(
1459
+ `Validation error: ${JSON.stringify(error)}`
1460
+ );
1461
+ throw error;
1462
+ }
1463
+
1464
+ const user = { id: data.id, name: data.name };
1465
+ yield* Effect.logInfo(
1466
+ `Successfully parsed user: ${JSON.stringify(user)}`
1467
+ );
1468
+ return user;
1469
+ } catch (e) {
1470
+ if (e instanceof ValidationError) {
1471
+ return yield* Effect.fail(e);
1472
+ }
1473
+ yield* Effect.logError(
1474
+ `Unexpected error: ${e instanceof Error ? e.message : String(e)}`
1475
+ );
1476
+ throw e;
1477
+ }
1478
+ }),
1479
+ }),
1480
+ }) {}
1481
+
1482
+ // Define test service
1483
+ class TestService extends Effect.Service<TestService>()("TestService", {
1484
+ sync: () => {
1485
+ // Create instance methods
1486
+ const printCause = (
1487
+ prefix: string,
1488
+ cause: Cause.Cause<unknown>
1489
+ ): Effect.Effect<void, never, never> =>
1490
+ Effect.gen(function* () {
1491
+ yield* Effect.logInfo(`\n=== ${prefix} ===`);
1492
+
1493
+ if (Cause.isDie(cause)) {
1494
+ const defect = Cause.failureOption(cause);
1495
+ if (defect._tag === "Some") {
1496
+ const error = defect.value as Error;
1497
+ yield* Effect.logError("Defect (unexpected error)");
1498
+ yield* Effect.logError(`Message: ${error.message}`);
1499
+ yield* Effect.logError(
1500
+ `Stack: ${error.stack?.split("\n")[1]?.trim() ?? "N/A"}`
1501
+ );
1502
+ }
1503
+ } else if (Cause.isFailure(cause)) {
1504
+ const error = Cause.failureOption(cause);
1505
+ yield* Effect.logWarning("Expected failure");
1506
+ yield* Effect.logWarning(`Error: ${JSON.stringify(error)}`);
1507
+ }
1508
+
1509
+ // Don't return an Effect inside Effect.gen, just return the value directly
1510
+ return void 0;
1511
+ });
1512
+
1513
+ const runScenario = <E, A extends { [key: string]: any }>(
1514
+ name: string,
1515
+ program: Effect.Effect<A, E>
1516
+ ): Effect.Effect<void, never, never> =>
1517
+ Effect.gen(function* () {
1518
+ yield* Effect.logInfo(`\n=== Testing: ${name} ===`);
1519
+
1520
+ type TestError = {
1521
+ readonly _tag: "error";
1522
+ readonly cause: Cause.Cause<E>;
1523
+ };
1524
+
1525
+ const result = yield* Effect.catchAllCause(program, (cause) =>
1526
+ Effect.succeed({ _tag: "error" as const, cause } as TestError)
1527
+ );
1528
+
1529
+ if ("cause" in result) {
1530
+ yield* printCause("Error details", result.cause);
1531
+ } else {
1532
+ yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);
1533
+ }
1534
+
1535
+ // Don't return an Effect inside Effect.gen, just return the value directly
1536
+ return void 0;
1537
+ });
1538
+
1539
+ // Return bound methods
1540
+ return {
1541
+ printCause,
1542
+ runScenario,
1543
+ };
1544
+ },
1545
+ }) {}
1546
+
1547
+ // Create program with proper error handling
1548
+ const program = Effect.gen(function* () {
1549
+ const db = yield* DatabaseService;
1550
+ const users = yield* UserService;
1551
+ const test = yield* TestService;
1552
+
1553
+ yield* Effect.logInfo("=== Starting Error Handling Tests ===");
1554
+
1555
+ // Test expected database errors
1556
+ yield* test.runScenario(
1557
+ "Expected database error",
1558
+ Effect.gen(function* () {
1559
+ const result = yield* Effect.retry(
1560
+ db.connect({ url: "" }),
1561
+ Schedule.exponential(100)
1562
+ ).pipe(
1563
+ Effect.timeout(Duration.seconds(5)),
1564
+ Effect.catchAll(() => Effect.fail("Connection timeout"))
1565
+ );
1566
+ return result;
1567
+ })
1568
+ );
1569
+
1570
+ // Test unexpected connection errors
1571
+ yield* test.runScenario(
1572
+ "Unexpected connection error",
1573
+ Effect.gen(function* () {
1574
+ const result = yield* Effect.retry(
1575
+ db.connect({ url: "invalid" }),
1576
+ Schedule.recurs(3)
1577
+ ).pipe(
1578
+ Effect.catchAllCause((cause) =>
1579
+ Effect.gen(function* () {
1580
+ yield* Effect.logError("Failed after 3 retries");
1581
+ yield* Effect.logError(Cause.pretty(cause));
1582
+ return yield* Effect.fail("Max retries exceeded");
1583
+ })
1584
+ )
1585
+ );
1586
+ return result;
1587
+ })
1588
+ );
1589
+
1590
+ // Test user validation with recovery
1591
+ yield* test.runScenario(
1592
+ "Valid user data",
1593
+ Effect.gen(function* () {
1594
+ const result = yield* users
1595
+ .parseUser({ id: "1", name: "John" })
1596
+ .pipe(
1597
+ Effect.orElse(() =>
1598
+ Effect.succeed({ id: "default", name: "Default User" })
1599
+ )
1600
+ );
1601
+ return result;
1602
+ })
1603
+ );
1604
+
1605
+ // Test concurrent error handling with timeout
1606
+ yield* test.runScenario(
1607
+ "Concurrent operations",
1608
+ Effect.gen(function* () {
1609
+ const results = yield* Effect.all(
1610
+ [
1611
+ db.connect({ url: "" }).pipe(
1612
+ Effect.timeout(Duration.seconds(1)),
1613
+ Effect.catchAll(() => Effect.succeed({ success: true }))
1614
+ ),
1615
+ users.parseUser({ id: "invalid" }).pipe(
1616
+ Effect.timeout(Duration.seconds(1)),
1617
+ Effect.catchAll(() =>
1618
+ Effect.succeed({ id: "timeout", name: "Timeout" })
1619
+ )
1620
+ ),
1621
+ ],
1622
+ { concurrency: 2 }
1623
+ );
1624
+ return results;
1625
+ })
1626
+ );
1627
+
1628
+ yield* Effect.logInfo("\n=== Error Handling Tests Complete ===");
1629
+
1630
+ // Don't return an Effect inside Effect.gen, just return the value directly
1631
+ return void 0;
1632
+ });
1633
+
1634
+ // Run the program with all services
1635
+ Effect.runPromise(
1636
+ Effect.provide(
1637
+ Effect.provide(
1638
+ Effect.provide(program, TestService.Default),
1639
+ DatabaseService.Default
1640
+ ),
1641
+ UserService.Default
1642
+ )
1643
+ );
1644
+ ```
1645
+
1646
+ **Explanation:**
1647
+ By inspecting the `Cause`, you can distinguish between expected and unexpected
1648
+ failures, logging or escalating as appropriate.
1649
+
1650
+ **Anti-Pattern:**
1651
+
1652
+ Using a simple `Effect.catchAll` can dangerously conflate expected errors and
1653
+ unexpected defects, masking critical bugs as recoverable errors.
1654
+
1655
+ **Rationale:**
1656
+
1657
+ To build truly resilient applications, differentiate between known business
1658
+ errors (`Fail`) and unknown defects (`Die`). Use `Effect.catchAllCause` to
1659
+ inspect the full `Cause` of a failure.
1660
+
1661
+
1662
+ The `Cause` object explains _why_ an effect failed. A `Fail` is an expected
1663
+ error (e.g., `ValidationError`). A `Die` is an unexpected defect (e.g., a
1664
+ thrown exception). They should be handled differently.
1665
+
1666
+ ---
1667
+
1668
+