@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,1212 @@
1
+ ---
2
+ name: effect-patterns-error-handling
3
+ description: Effect-TS patterns for Error Handling. Use when working with error handling in Effect-TS applications.
4
+ ---
5
+ # Effect-TS Patterns: Error Handling
6
+ This skill provides 3 curated Effect-TS patterns for error handling.
7
+ Use this skill when working on tasks related to:
8
+ - error handling
9
+ - Best practices in Effect-TS applications
10
+ - Real-world patterns and solutions
11
+
12
+ ---
13
+
14
+ ## 🟡 Intermediate Patterns
15
+
16
+ ### Error Handling Pattern 1: Accumulating Multiple Errors
17
+
18
+ **Rule:** Use error accumulation to report all problems at once rather than failing early, critical for validation and batch operations.
19
+
20
+ **Good Example:**
21
+
22
+ This example demonstrates error accumulation patterns.
23
+
24
+ ```typescript
25
+ import { Effect, Data, Cause } from "effect";
26
+
27
+ interface ValidationError {
28
+ field: string;
29
+ message: string;
30
+ value?: unknown;
31
+ }
32
+
33
+ interface ProcessingResult<T> {
34
+ successes: T[];
35
+ errors: ValidationError[];
36
+ }
37
+
38
+ // Example 1: Form validation with error accumulation
39
+ const program = Effect.gen(function* () {
40
+ console.log(`\n[ERROR ACCUMULATION] Collecting multiple errors\n`);
41
+
42
+ // Form data
43
+ interface FormData {
44
+ name: string;
45
+ email: string;
46
+ age: number;
47
+ phone: string;
48
+ }
49
+
50
+ const validateForm = (data: FormData): ValidationError[] => {
51
+ const errors: ValidationError[] = [];
52
+
53
+ // Validation 1: Name
54
+ if (!data.name || data.name.trim().length === 0) {
55
+ errors.push({
56
+ field: "name",
57
+ message: "Name is required",
58
+ value: data.name,
59
+ });
60
+ } else if (data.name.length < 2) {
61
+ errors.push({
62
+ field: "name",
63
+ message: "Name must be at least 2 characters",
64
+ value: data.name,
65
+ });
66
+ }
67
+
68
+ // Validation 2: Email
69
+ if (!data.email) {
70
+ errors.push({
71
+ field: "email",
72
+ message: "Email is required",
73
+ value: data.email,
74
+ });
75
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
76
+ errors.push({
77
+ field: "email",
78
+ message: "Email format invalid",
79
+ value: data.email,
80
+ });
81
+ }
82
+
83
+ // Validation 3: Age
84
+ if (data.age < 0 || data.age > 150) {
85
+ errors.push({
86
+ field: "age",
87
+ message: "Age must be between 0 and 150",
88
+ value: data.age,
89
+ });
90
+ }
91
+
92
+ // Validation 4: Phone
93
+ if (data.phone && !/^\d{3}-\d{3}-\d{4}$/.test(data.phone)) {
94
+ errors.push({
95
+ field: "phone",
96
+ message: "Phone must be in format XXX-XXX-XXXX",
97
+ value: data.phone,
98
+ });
99
+ }
100
+
101
+ return errors;
102
+ };
103
+
104
+ // Example 1: Form with multiple errors
105
+ console.log(`[1] Form validation with multiple errors:\n`);
106
+
107
+ const invalidForm: FormData = {
108
+ name: "",
109
+ email: "not-an-email",
110
+ age: 200,
111
+ phone: "invalid",
112
+ };
113
+
114
+ const validationErrors = validateForm(invalidForm);
115
+
116
+ yield* Effect.log(`[VALIDATION] Found ${validationErrors.length} errors:\n`);
117
+
118
+ for (const error of validationErrors) {
119
+ yield* Effect.log(` ✗ ${error.field}: ${error.message}`);
120
+ }
121
+
122
+ // Example 2: Batch processing with partial success
123
+ console.log(`\n[2] Batch processing (accumulate successes and failures):\n`);
124
+
125
+ interface Record {
126
+ id: string;
127
+ data: string;
128
+ }
129
+
130
+ const processRecord = (record: Record): Result<string> => {
131
+ if (record.id.length === 0) {
132
+ return { success: false, error: "Missing ID" };
133
+ }
134
+
135
+ if (record.data.includes("ERROR")) {
136
+ return { success: false, error: "Invalid data" };
137
+ }
138
+
139
+ return { success: true, value: `processed-${record.id}` };
140
+ };
141
+
142
+ interface Result<T> {
143
+ success: boolean;
144
+ value?: T;
145
+ error?: string;
146
+ }
147
+
148
+ const records: Record[] = [
149
+ { id: "rec1", data: "ok" },
150
+ { id: "", data: "ok" }, // Error: missing ID
151
+ { id: "rec3", data: "ok" },
152
+ { id: "rec4", data: "ERROR" }, // Error: invalid data
153
+ { id: "rec5", data: "ok" },
154
+ ];
155
+
156
+ const results: ProcessingResult<string> = {
157
+ successes: [],
158
+ errors: [],
159
+ };
160
+
161
+ for (const record of records) {
162
+ const result = processRecord(record);
163
+
164
+ if (result.success) {
165
+ results.successes.push(result.value!);
166
+ } else {
167
+ results.errors.push({
168
+ field: record.id || "unknown",
169
+ message: result.error!,
170
+ });
171
+ }
172
+ }
173
+
174
+ yield* Effect.log(
175
+ `[BATCH] Processed ${records.length} records`
176
+ );
177
+ yield* Effect.log(`[BATCH] ✓ ${results.successes.length} succeeded`);
178
+ yield* Effect.log(`[BATCH] ✗ ${results.errors.length} failed\n`);
179
+
180
+ for (const success of results.successes) {
181
+ yield* Effect.log(` ✓ ${success}`);
182
+ }
183
+
184
+ for (const error of results.errors) {
185
+ yield* Effect.log(` ✗ [${error.field}] ${error.message}`);
186
+ }
187
+
188
+ // Example 3: Multi-step validation with error accumulation
189
+ console.log(`\n[3] Multi-step validation (all checks run):\n`);
190
+
191
+ interface ServiceHealth {
192
+ diskSpace: boolean;
193
+ memory: boolean;
194
+ network: boolean;
195
+ database: boolean;
196
+ }
197
+
198
+ const diagnostics: ValidationError[] = [];
199
+
200
+ // Check 1: Disk space
201
+ const diskFree = 50; // MB
202
+
203
+ if (diskFree < 100) {
204
+ diagnostics.push({
205
+ field: "disk-space",
206
+ message: `Only ${diskFree}MB free (need 100MB)`,
207
+ value: diskFree,
208
+ });
209
+ }
210
+
211
+ // Check 2: Memory
212
+ const memUsage = 95; // percent
213
+
214
+ if (memUsage > 85) {
215
+ diagnostics.push({
216
+ field: "memory",
217
+ message: `Using ${memUsage}% (threshold: 85%)`,
218
+ value: memUsage,
219
+ });
220
+ }
221
+
222
+ // Check 3: Network
223
+ const latency = 500; // ms
224
+
225
+ if (latency > 200) {
226
+ diagnostics.push({
227
+ field: "network",
228
+ message: `Latency ${latency}ms (threshold: 200ms)`,
229
+ value: latency,
230
+ });
231
+ }
232
+
233
+ // Check 4: Database
234
+ const dbConnections = 95;
235
+ const dbMax = 100;
236
+
237
+ if (dbConnections > dbMax * 0.8) {
238
+ diagnostics.push({
239
+ field: "database",
240
+ message: `${dbConnections}/${dbMax} connections (80% threshold)`,
241
+ value: dbConnections,
242
+ });
243
+ }
244
+
245
+ if (diagnostics.length === 0) {
246
+ yield* Effect.log(`[HEALTH] ✓ All systems normal\n`);
247
+ } else {
248
+ yield* Effect.log(
249
+ `[HEALTH] ✗ ${diagnostics.length} issue(s) detected:\n`
250
+ );
251
+
252
+ for (const diag of diagnostics) {
253
+ yield* Effect.log(` ⚠ ${diag.field}: ${diag.message}`);
254
+ }
255
+ }
256
+
257
+ // Example 4: Error collection with retry decisions
258
+ console.log(`\n[4] Error collection for retry strategy:\n`);
259
+
260
+ interface ErrorWithContext {
261
+ operation: string;
262
+ error: string;
263
+ retryable: boolean;
264
+ timestamp: Date;
265
+ }
266
+
267
+ const operationErrors: ErrorWithContext[] = [];
268
+
269
+ const operations = [
270
+ { name: "fetch-config", fail: false },
271
+ { name: "connect-db", fail: true },
272
+ { name: "load-cache", fail: true },
273
+ { name: "start-server", fail: false },
274
+ ];
275
+
276
+ for (const op of operations) {
277
+ if (op.fail) {
278
+ operationErrors.push({
279
+ operation: op.name,
280
+ error: "Operation failed",
281
+ retryable: op.name !== "fetch-config",
282
+ timestamp: new Date(),
283
+ });
284
+ }
285
+ }
286
+
287
+ yield* Effect.log(`[OPERATIONS] ${operationErrors.length} errors:\n`);
288
+
289
+ for (const err of operationErrors) {
290
+ const status = err.retryable ? "🔄 retryable" : "❌ non-retryable";
291
+ yield* Effect.log(` ${status}: ${err.operation}`);
292
+ }
293
+
294
+ if (operationErrors.every((e) => e.retryable)) {
295
+ yield* Effect.log(`\n[DECISION] All errors retryable, will retry\n`);
296
+ } else {
297
+ yield* Effect.log(`\n[DECISION] Some non-retryable errors, manual intervention needed\n`);
298
+ }
299
+ });
300
+
301
+ Effect.runPromise(program);
302
+ ```
303
+
304
+ ---
305
+
306
+ **Rationale:**
307
+
308
+ Error accumulation strategies:
309
+
310
+ - **Collect errors**: Gather all failures before reporting
311
+ - **Fail late**: Continue processing despite errors
312
+ - **Contextual errors**: Keep error location/operation info
313
+ - **Error summary**: Aggregate for reporting
314
+ - **Partial success**: Return valid results + errors
315
+
316
+ Pattern: Use `Cause` aggregation, `Result` types, or custom error structures
317
+
318
+ ---
319
+
320
+
321
+ Failing fast causes problems:
322
+
323
+ **Problem 1: Form validation**
324
+ - User submits form with 10 field errors
325
+ - Fail on first error: "Name required"
326
+ - User fixes name, submits again
327
+ - New error: "Email invalid"
328
+ - User submits 10 times before fixing all errors
329
+ - Frustration, reduced productivity
330
+
331
+ **Problem 2: Batch processing**
332
+ - Process 1000 records, fail on record 5
333
+ - 995 records not processed
334
+ - User manually retries
335
+ - Repeats for each error type
336
+ - Inefficient
337
+
338
+ **Problem 3: System diagnostics**
339
+ - Service health check fails
340
+ - Report: "Check 1 failed"
341
+ - Fix check 1, service still down
342
+ - Hidden problem: checks 2, 3, and 4 also failed
343
+ - Time wasted diagnosing
344
+
345
+ Solutions:
346
+
347
+ **Error accumulation**:
348
+ - Run all validations
349
+ - Collect errors
350
+ - Report all problems
351
+ - User fixes once, not 10 times
352
+
353
+ **Partial success**:
354
+ - Process all records
355
+ - Track successes and failures
356
+ - Return: "950 succeeded, 50 failed"
357
+ - No re-processing
358
+
359
+ **Comprehensive diagnostics**:
360
+ - Run all checks
361
+ - Report all failures
362
+ - Quick root cause analysis
363
+ - Faster resolution
364
+
365
+ ---
366
+
367
+ ---
368
+
369
+
370
+ ## 🟠 Advanced Patterns
371
+
372
+ ### Error Handling Pattern 2: Error Propagation and Chains
373
+
374
+ **Rule:** Use error propagation to preserve context through effect chains, enabling debugging and recovery at the right abstraction level.
375
+
376
+ **Good Example:**
377
+
378
+ This example demonstrates error propagation with context.
379
+
380
+ ```typescript
381
+ import { Effect, Data, Cause } from "effect";
382
+
383
+ // Domain-specific errors with context
384
+ class DatabaseError extends Data.TaggedError("DatabaseError")<{
385
+ query: string;
386
+ parameters: unknown[];
387
+ cause: Error;
388
+ }> {}
389
+
390
+ class NetworkError extends Data.TaggedError("NetworkError")<{
391
+ endpoint: string;
392
+ method: string;
393
+ statusCode?: number;
394
+ cause: Error;
395
+ }> {}
396
+
397
+ class ValidationError extends Data.TaggedError("ValidationError")<{
398
+ field: string;
399
+ value: unknown;
400
+ reason: string;
401
+ }> {}
402
+
403
+ class BusinessLogicError extends Data.TaggedError("BusinessLogicError")<{
404
+ operation: string;
405
+ context: Record<string, unknown>;
406
+ originalError: Error;
407
+ }> {}
408
+
409
+ const program = Effect.gen(function* () {
410
+ console.log(`\n[ERROR PROPAGATION] Error chains with context\n`);
411
+
412
+ // Example 1: Simple error propagation
413
+ console.log(`[1] Error propagation through layers:\n`);
414
+
415
+ const lowLevelOperation = Effect.gen(function* () {
416
+ yield* Effect.log(`[LAYER 1] Low-level operation starting`);
417
+
418
+ yield* Effect.fail(new Error("File not found"));
419
+ });
420
+
421
+ const midLevelOperation = lowLevelOperation.pipe(
422
+ Effect.mapError((error) =>
423
+ new DatabaseError({
424
+ query: "SELECT * FROM users",
425
+ parameters: ["id=123"],
426
+ cause: error instanceof Error ? error : new Error(String(error)),
427
+ })
428
+ )
429
+ );
430
+
431
+ const highLevelOperation = midLevelOperation.pipe(
432
+ Effect.catchTag("DatabaseError", (dbError) =>
433
+ Effect.gen(function* () {
434
+ yield* Effect.log(`[LAYER 3] Caught database error`);
435
+ yield* Effect.log(`[LAYER 3] Query: ${dbError.query}`);
436
+ yield* Effect.log(`[LAYER 3] Cause: ${dbError.cause.message}`);
437
+
438
+ // Recovery decision
439
+ return "fallback-value";
440
+ })
441
+ )
442
+ );
443
+
444
+ const result1 = yield* highLevelOperation;
445
+
446
+ yield* Effect.log(`[RESULT] Recovered with: ${result1}\n`);
447
+
448
+ // Example 2: Error context accumulation
449
+ console.log(`[2] Accumulating context through layers:\n`);
450
+
451
+ interface ErrorContext {
452
+ timestamp: Date;
453
+ operation: string;
454
+ userId?: string;
455
+ requestId: string;
456
+ }
457
+
458
+ const errorWithContext = (context: ErrorContext) =>
459
+ Effect.fail(
460
+ new BusinessLogicError({
461
+ operation: context.operation,
462
+ context: {
463
+ userId: context.userId,
464
+ timestamp: context.timestamp.toISOString(),
465
+ requestId: context.requestId,
466
+ },
467
+ originalError: new Error("Operation failed"),
468
+ })
469
+ );
470
+
471
+ const myContext: ErrorContext = {
472
+ timestamp: new Date(),
473
+ operation: "process-payment",
474
+ userId: "user-123",
475
+ requestId: "req-abc-def",
476
+ };
477
+
478
+ const withContextRecovery = errorWithContext(myContext).pipe(
479
+ Effect.mapError((error) => {
480
+ // Log complete context
481
+ return {
482
+ ...error,
483
+ enriched: true,
484
+ additionalInfo: {
485
+ serviceName: "payment-service",
486
+ environment: "production",
487
+ version: "1.2.3",
488
+ },
489
+ };
490
+ }),
491
+ Effect.catchAll((error) =>
492
+ Effect.gen(function* () {
493
+ yield* Effect.log(`[ERROR CAUGHT] ${error.operation}`);
494
+ yield* Effect.log(`[CONTEXT] ${JSON.stringify(error.context, null, 2)}`);
495
+ return "recovered";
496
+ })
497
+ )
498
+ );
499
+
500
+ yield* withContextRecovery;
501
+
502
+ // Example 3: Network error with retry context
503
+ console.log(`\n[3] Network errors with retry context:\n`);
504
+
505
+ interface RetryContext {
506
+ attempt: number;
507
+ maxAttempts: number;
508
+ delay: number;
509
+ }
510
+
511
+ let attemptCount = 0;
512
+
513
+ const networkCall = Effect.gen(function* () {
514
+ attemptCount++;
515
+
516
+ yield* Effect.log(`[ATTEMPT] ${attemptCount}/3`);
517
+
518
+ if (attemptCount < 3) {
519
+ yield* Effect.fail(
520
+ new NetworkError({
521
+ endpoint: "https://api.example.com/data",
522
+ method: "GET",
523
+ statusCode: 503,
524
+ cause: new Error("Service Unavailable"),
525
+ })
526
+ );
527
+ }
528
+
529
+ return "success";
530
+ });
531
+
532
+ const withRetryContext = Effect.gen(function* () {
533
+ let lastError: NetworkError | null = null;
534
+
535
+ for (let i = 1; i <= 3; i++) {
536
+ const result = yield* networkCall.pipe(
537
+ Effect.catchTag("NetworkError", (error) => {
538
+ lastError = error;
539
+
540
+ yield* Effect.log(
541
+ `[RETRY] Attempt ${i} failed: ${error.statusCode}`
542
+ );
543
+
544
+ if (i < 3) {
545
+ yield* Effect.log(`[RETRY] Waiting before retry...`);
546
+ }
547
+
548
+ return Effect.fail(error);
549
+ })
550
+ ).pipe(
551
+ Effect.tap(() => Effect.log(`[SUCCESS] Connected on attempt ${i}`))
552
+ ).pipe(
553
+ Effect.catchAll(() => Effect.succeed(null))
554
+ );
555
+
556
+ if (result !== null) {
557
+ return result;
558
+ }
559
+ }
560
+
561
+ if (lastError) {
562
+ yield* Effect.fail(lastError);
563
+ }
564
+
565
+ return null;
566
+ });
567
+
568
+ const networkResult = yield* withRetryContext.pipe(
569
+ Effect.catchAll((error) =>
570
+ Effect.gen(function* () {
571
+ yield* Effect.log(`[EXHAUSTED] All retries failed`);
572
+ return "fallback";
573
+ })
574
+ )
575
+ );
576
+
577
+ yield* Effect.log(`\n`);
578
+
579
+ // Example 4: Multi-layer error transformation
580
+ console.log(`[4] Error transformation between layers:\n`);
581
+
582
+ const layer1Error = Effect.gen(function* () {
583
+ yield* Effect.fail(new Error("Raw system error"));
584
+ });
585
+
586
+ // Layer 2: Convert to domain error
587
+ const layer2 = layer1Error.pipe(
588
+ Effect.mapError((error) =>
589
+ new DatabaseError({
590
+ query: "SELECT ...",
591
+ parameters: [],
592
+ cause: error instanceof Error ? error : new Error(String(error)),
593
+ })
594
+ )
595
+ );
596
+
597
+ // Layer 3: Convert to business error
598
+ const layer3 = layer2.pipe(
599
+ Effect.mapError((dbError) =>
600
+ new BusinessLogicError({
601
+ operation: "fetch-user-profile",
602
+ context: {
603
+ dbError: dbError.query,
604
+ },
605
+ originalError: dbError.cause,
606
+ })
607
+ )
608
+ );
609
+
610
+ // Layer 4: Return user-friendly error
611
+ const userFacingError = layer3.pipe(
612
+ Effect.mapError((bizError) => ({
613
+ message: "Unable to load profile",
614
+ code: "PROFILE_LOAD_FAILED",
615
+ originalError: bizError.originalError.message,
616
+ })),
617
+ Effect.catchAll((userError) =>
618
+ Effect.gen(function* () {
619
+ yield* Effect.log(`[USER MESSAGE] ${userError.message}`);
620
+ yield* Effect.log(`[CODE] ${userError.code}`);
621
+ yield* Effect.log(`[DEBUG] ${userError.originalError}`);
622
+ return null;
623
+ })
624
+ )
625
+ );
626
+
627
+ yield* userFacingError;
628
+
629
+ // Example 5: Error aggregation in concurrent operations
630
+ console.log(`\n[5] Error propagation in concurrent operations:\n`);
631
+
632
+ const operation = (id: number, shouldFail: boolean) =>
633
+ Effect.gen(function* () {
634
+ if (shouldFail) {
635
+ yield* Effect.fail(
636
+ new Error(`Operation ${id} failed`)
637
+ );
638
+ }
639
+
640
+ return `result-${id}`;
641
+ });
642
+
643
+ const concurrent = Effect.gen(function* () {
644
+ const results = yield* Effect.all(
645
+ [
646
+ operation(1, false),
647
+ operation(2, true),
648
+ operation(3, false),
649
+ ],
650
+ { concurrency: 3 }
651
+ ).pipe(
652
+ Effect.catchAll((errors) =>
653
+ Effect.gen(function* () {
654
+ yield* Effect.log(`[CONCURRENT] Caught aggregated errors`);
655
+
656
+ // In real code, Cause provides error details
657
+ yield* Effect.log(`[ERROR] Errors encountered during concurrent execution`);
658
+
659
+ return [];
660
+ })
661
+ )
662
+ );
663
+
664
+ return results;
665
+ });
666
+
667
+ yield* concurrent;
668
+
669
+ yield* Effect.log(`\n[DEMO] Error propagation complete`);
670
+ });
671
+
672
+ Effect.runPromise(program);
673
+ ```
674
+
675
+ ---
676
+
677
+ **Rationale:**
678
+
679
+ Error propagation preserves context:
680
+
681
+ - **Cause chain**: Keep original error + context
682
+ - **Stack trace**: Preserve execution history
683
+ - **Error context**: Add operation name, parameters
684
+ - **Error mapping**: Transform errors between layers
685
+ - **Recovery points**: Decide where to handle errors
686
+
687
+ Pattern: Use `mapError()`, `tapError()`, `catchAll()`, `Cause.prettyPrint()`
688
+
689
+ ---
690
+
691
+
692
+ Loss of error context causes problems:
693
+
694
+ **Problem 1: Useless error messages**
695
+ - User sees: "Error: null"
696
+ - Debugging: Where did it come from? When? Why?
697
+ - Wasted hours searching logs
698
+
699
+ **Problem 2: Wrong recovery layer**
700
+ - Network error → recovered at business logic layer (inefficient)
701
+ - Should be recovered at network layer → retry, exponential backoff
702
+
703
+ **Problem 3: Error context loss**
704
+ - Database connection failed
705
+ - But which database? Which query? With what parameters?
706
+ - Logs show "Connection failed" (not actionable)
707
+
708
+ **Problem 4: Hidden root cause**
709
+ - Effect 1 fails → triggers Effect 2 → different error
710
+ - Developer sees Effect 2 error
711
+ - Doesn't know Effect 1 was root cause
712
+ - Fixes wrong thing
713
+
714
+ Solutions:
715
+
716
+ **Error context**:
717
+ - Include operation name
718
+ - Include relevant parameters
719
+ - Include timestamps
720
+ - Include retry count
721
+
722
+ **Error cause chains**:
723
+ - Keep original error
724
+ - Add context at each layer
725
+ - `mapError()` to transform
726
+ - `tapError()` to log context
727
+
728
+ **Recovery layers**:
729
+ - Low-level: Retry network requests
730
+ - Mid-level: Transform domain errors
731
+ - High-level: Convert to user-friendly messages
732
+
733
+ ---
734
+
735
+ ---
736
+
737
+ ### Error Handling Pattern 3: Custom Error Strategies
738
+
739
+ **Rule:** Use tagged errors and custom error types to enable type-safe error handling and business-logic-aware recovery strategies.
740
+
741
+ **Good Example:**
742
+
743
+ This example demonstrates custom error strategies.
744
+
745
+ ```typescript
746
+ import { Effect, Data, Schedule } from "effect";
747
+
748
+ // Custom domain errors
749
+ class NetworkError extends Data.TaggedError("NetworkError")<{
750
+ endpoint: string;
751
+ statusCode?: number;
752
+ retryable: boolean;
753
+ }> {}
754
+
755
+ class ValidationError extends Data.TaggedError("ValidationError")<{
756
+ field: string;
757
+ reason: string;
758
+ }> {}
759
+
760
+ class AuthenticationError extends Data.TaggedError("AuthenticationError")<{
761
+ reason: "invalid-token" | "expired-token" | "missing-token";
762
+ }> {}
763
+
764
+ class PermissionError extends Data.TaggedError("PermissionError")<{
765
+ resource: string;
766
+ action: string;
767
+ }> {}
768
+
769
+ class RateLimitError extends Data.TaggedError("RateLimitError")<{
770
+ retryAfter: number; // milliseconds
771
+ }> {}
772
+
773
+ class NotFoundError extends Data.TaggedError("NotFoundError")<{
774
+ resource: string;
775
+ id: string;
776
+ }> {}
777
+
778
+ // Recovery strategy selector
779
+ const selectRecoveryStrategy = (
780
+ error: Error
781
+ ): "retry" | "fallback" | "fail" | "user-message" => {
782
+ if (error instanceof NetworkError && error.retryable) {
783
+ return "retry";
784
+ }
785
+
786
+ if (error instanceof RateLimitError) {
787
+ return "retry"; // With backoff
788
+ }
789
+
790
+ if (error instanceof ValidationError) {
791
+ return "user-message"; // User can fix
792
+ }
793
+
794
+ if (error instanceof NotFoundError) {
795
+ return "fallback"; // Use empty result
796
+ }
797
+
798
+ if (
799
+ error instanceof AuthenticationError &&
800
+ error.reason === "expired-token"
801
+ ) {
802
+ return "retry"; // Refresh token
803
+ }
804
+
805
+ if (error instanceof PermissionError) {
806
+ return "fail"; // Don't retry
807
+ }
808
+
809
+ return "fail"; // Default: don't retry
810
+ };
811
+
812
+ const program = Effect.gen(function* () {
813
+ console.log(
814
+ `\n[CUSTOM ERROR STRATEGIES] Domain-aware error handling\n`
815
+ );
816
+
817
+ // Example 1: Type-safe error handling
818
+ console.log(`[1] Type-safe error catching:\n`);
819
+
820
+ const operation1 = Effect.fail(
821
+ new ValidationError({
822
+ field: "email",
823
+ reason: "Invalid format",
824
+ })
825
+ );
826
+
827
+ const handled1 = operation1.pipe(
828
+ Effect.catchTag("ValidationError", (error) =>
829
+ Effect.gen(function* () {
830
+ yield* Effect.log(`[CAUGHT] Validation error`);
831
+ yield* Effect.log(` Field: ${error.field}`);
832
+ yield* Effect.log(` Reason: ${error.reason}\n`);
833
+
834
+ return "validation-failed";
835
+ })
836
+ )
837
+ );
838
+
839
+ yield* handled1;
840
+
841
+ // Example 2: Multiple error types with different recovery
842
+ console.log(`[2] Different recovery per error type:\n`);
843
+
844
+ interface ApiResponse {
845
+ status: number;
846
+ body?: unknown;
847
+ }
848
+
849
+ const callApi = (shouldFail: "network" | "validation" | "ratelimit" | "success") =>
850
+ Effect.gen(function* () {
851
+ switch (shouldFail) {
852
+ case "network":
853
+ yield* Effect.fail(
854
+ new NetworkError({
855
+ endpoint: "https://api.example.com/data",
856
+ statusCode: 503,
857
+ retryable: true,
858
+ })
859
+ );
860
+
861
+ case "validation":
862
+ yield* Effect.fail(
863
+ new ValidationError({
864
+ field: "id",
865
+ reason: "Must be numeric",
866
+ })
867
+ );
868
+
869
+ case "ratelimit":
870
+ yield* Effect.fail(
871
+ new RateLimitError({
872
+ retryAfter: 5000,
873
+ })
874
+ );
875
+
876
+ case "success":
877
+ return { status: 200, body: { id: 123 } };
878
+ }
879
+ });
880
+
881
+ // Test each error type
882
+ const testCases = ["network", "validation", "ratelimit", "success"] as const;
883
+
884
+ for (const testCase of testCases) {
885
+ const strategy = yield* callApi(testCase).pipe(
886
+ Effect.catchTag("NetworkError", (error) =>
887
+ Effect.gen(function* () {
888
+ yield* Effect.log(
889
+ `[NETWORK] Retryable: ${error.retryable}, Status: ${error.statusCode}`
890
+ );
891
+
892
+ return "will-retry";
893
+ })
894
+ ),
895
+ Effect.catchTag("ValidationError", (error) =>
896
+ Effect.gen(function* () {
897
+ yield* Effect.log(
898
+ `[VALIDATION] ${error.field}: ${error.reason} (no retry)`
899
+ );
900
+
901
+ return "user-must-fix";
902
+ })
903
+ ),
904
+ Effect.catchTag("RateLimitError", (error) =>
905
+ Effect.gen(function* () {
906
+ yield* Effect.log(
907
+ `[RATE-LIMIT] Retry after ${error.retryAfter}ms`
908
+ );
909
+
910
+ return "retry-with-backoff";
911
+ })
912
+ ),
913
+ Effect.catchAll((error) =>
914
+ Effect.gen(function* () {
915
+ yield* Effect.log(`[SUCCESS] Got response`);
916
+
917
+ return "completed";
918
+ })
919
+ )
920
+ );
921
+
922
+ yield* Effect.log(` Strategy: ${strategy}\n`);
923
+ }
924
+
925
+ // Example 3: Custom retry strategy based on error
926
+ console.log(`[3] Error-specific retry strategies:\n`);
927
+
928
+ let attemptCount = 0;
929
+
930
+ const networkOperation = Effect.gen(function* () {
931
+ attemptCount++;
932
+
933
+ yield* Effect.log(`[ATTEMPT] ${attemptCount}`);
934
+
935
+ if (attemptCount === 1) {
936
+ yield* Effect.fail(
937
+ new NetworkError({
938
+ endpoint: "api.example.com",
939
+ statusCode: 502,
940
+ retryable: true,
941
+ })
942
+ );
943
+ }
944
+
945
+ if (attemptCount === 2) {
946
+ yield* Effect.fail(
947
+ new RateLimitError({
948
+ retryAfter: 100,
949
+ })
950
+ );
951
+ }
952
+
953
+ return "success";
954
+ });
955
+
956
+ // Type-safe retry with error classification
957
+ let result3: string | null = null;
958
+
959
+ for (let i = 0; i < 3; i++) {
960
+ result3 = yield* networkOperation.pipe(
961
+ Effect.catchTag("NetworkError", (error) =>
962
+ Effect.gen(function* () {
963
+ if (error.retryable && i < 2) {
964
+ yield* Effect.log(`[RETRY] Network error is retryable`);
965
+
966
+ return null; // Signal to retry
967
+ }
968
+
969
+ yield* Effect.log(`[FAIL] Network error not retryable`);
970
+
971
+ return Effect.fail(error);
972
+ })
973
+ ),
974
+ Effect.catchTag("RateLimitError", (error) =>
975
+ Effect.gen(function* () {
976
+ yield* Effect.log(
977
+ `[BACKOFF] Rate limited, waiting ${error.retryAfter}ms`
978
+ );
979
+
980
+ yield* Effect.sleep(`${error.retryAfter} millis`);
981
+
982
+ return null; // Signal to retry
983
+ })
984
+ ),
985
+ Effect.catchAll((error) =>
986
+ Effect.gen(function* () {
987
+ yield* Effect.log(`[ERROR] Unhandled: ${error}`);
988
+
989
+ return Effect.fail(error);
990
+ })
991
+ )
992
+ ).pipe(
993
+ Effect.catchAll(() => Effect.succeed(null))
994
+ );
995
+
996
+ if (result3 !== null) {
997
+ break;
998
+ }
999
+ }
1000
+
1001
+ yield* Effect.log(`\n[RESULT] ${result3}\n`);
1002
+
1003
+ // Example 4: Error-aware business logic
1004
+ console.log(`[4] Business logic with error handling:\n`);
1005
+
1006
+ interface User {
1007
+ id: string;
1008
+ email: string;
1009
+ }
1010
+
1011
+ const loadUser = (id: string): Effect.Effect<User, NetworkError | NotFoundError> =>
1012
+ Effect.gen(function* () {
1013
+ if (id === "invalid") {
1014
+ yield* Effect.fail(
1015
+ new NotFoundError({
1016
+ resource: "user",
1017
+ id,
1018
+ })
1019
+ );
1020
+ }
1021
+
1022
+ if (id === "network-error") {
1023
+ yield* Effect.fail(
1024
+ new NetworkError({
1025
+ endpoint: "/api/users",
1026
+ retryable: true,
1027
+ })
1028
+ );
1029
+ }
1030
+
1031
+ return { id, email: `user-${id}@example.com` };
1032
+ });
1033
+
1034
+ const processUser = (id: string) =>
1035
+ loadUser(id).pipe(
1036
+ Effect.catchTag("NotFoundError", (error) =>
1037
+ Effect.gen(function* () {
1038
+ yield* Effect.log(
1039
+ `[BUSINESS] User not found: ${error.id}`
1040
+ );
1041
+
1042
+ // Return default/empty user
1043
+ return { id: "", email: "" };
1044
+ })
1045
+ ),
1046
+ Effect.catchTag("NetworkError", (error) =>
1047
+ Effect.gen(function* () {
1048
+ yield* Effect.log(
1049
+ `[BUSINESS] Network error, will retry from cache`
1050
+ );
1051
+
1052
+ return { id, email: "cached@example.com" };
1053
+ })
1054
+ )
1055
+ );
1056
+
1057
+ yield* processUser("valid-id");
1058
+
1059
+ yield* processUser("invalid");
1060
+
1061
+ yield* processUser("network-error");
1062
+
1063
+ // Example 5: Discriminated union for exhaustiveness
1064
+ console.log(`\n[5] Exhaustiveness checking (compile-time safety):\n`);
1065
+
1066
+ const classifyError = (
1067
+ error: NetworkError | ValidationError | AuthenticationError | PermissionError
1068
+ ): string => {
1069
+ switch (error._tag) {
1070
+ case "NetworkError":
1071
+ return `network: ${error.statusCode}`;
1072
+
1073
+ case "ValidationError":
1074
+ return `validation: ${error.field}`;
1075
+
1076
+ case "AuthenticationError":
1077
+ return `auth: ${error.reason}`;
1078
+
1079
+ case "PermissionError":
1080
+ return `permission: ${error.action}`;
1081
+
1082
+ // TypeScript ensures all cases covered
1083
+ default:
1084
+ const _exhaustive: never = error;
1085
+ return _exhaustive;
1086
+ }
1087
+ };
1088
+
1089
+ const testError = new ValidationError({
1090
+ field: "age",
1091
+ reason: "Must be >= 18",
1092
+ });
1093
+
1094
+ const classification = classifyError(testError);
1095
+
1096
+ yield* Effect.log(`[CLASSIFY] ${classification}`);
1097
+
1098
+ // Example 6: Recovery strategy chains
1099
+ console.log(`\n[6] Chained recovery strategies:\n`);
1100
+
1101
+ const resilientOperation = Effect.gen(function* () {
1102
+ yield* Effect.fail(
1103
+ new RateLimitError({
1104
+ retryAfter: 50,
1105
+ })
1106
+ );
1107
+ });
1108
+
1109
+ const withRecovery = resilientOperation.pipe(
1110
+ Effect.catchTag("RateLimitError", (error) =>
1111
+ Effect.gen(function* () {
1112
+ yield* Effect.log(
1113
+ `[STEP 1] Caught rate limit, waiting ${error.retryAfter}ms`
1114
+ );
1115
+
1116
+ yield* Effect.sleep(`${error.retryAfter} millis`);
1117
+
1118
+ // Try again
1119
+ return yield* Effect.succeed("recovered");
1120
+ })
1121
+ ),
1122
+ Effect.catchTag("NetworkError", (error) =>
1123
+ Effect.gen(function* () {
1124
+ if (error.retryable) {
1125
+ yield* Effect.log(`[STEP 2] Network error, retrying...`);
1126
+
1127
+ return "retry";
1128
+ }
1129
+
1130
+ return yield* Effect.fail(error);
1131
+ })
1132
+ ),
1133
+ Effect.catchAll((error) =>
1134
+ Effect.gen(function* () {
1135
+ yield* Effect.log(`[STEP 3] Final fallback`);
1136
+
1137
+ return "fallback";
1138
+ })
1139
+ )
1140
+ );
1141
+
1142
+ yield* withRecovery;
1143
+ });
1144
+
1145
+ Effect.runPromise(program);
1146
+ ```
1147
+
1148
+ ---
1149
+
1150
+ **Rationale:**
1151
+
1152
+ Custom error strategies enable business logic:
1153
+
1154
+ - **Tagged errors**: Effect.Data for type-safe errors
1155
+ - **Error classification**: Retryable, transient, permanent
1156
+ - **Domain semantics**: Business-meaning errors
1157
+ - **Recovery strategies**: Different per error type
1158
+ - **Error context**: Includes recovery hints
1159
+
1160
+ Pattern: Use `Data.TaggedError`, error discriminators, `catchTag()`
1161
+
1162
+ ---
1163
+
1164
+
1165
+ Generic errors prevent optimal recovery:
1166
+
1167
+ **Problem 1: One-size-fits-all retry**
1168
+ - Network timeout (transient, retry with backoff)
1169
+ - Invalid API key (permanent, don't retry)
1170
+ - Both treated same = wrong recovery
1171
+
1172
+ **Problem 2: Lost business intent**
1173
+ - System error: "Connection refused"
1174
+ - Business meaning: Unclear
1175
+ - User message: "Something went wrong" (not helpful)
1176
+
1177
+ **Problem 3: Wrong recovery layer**
1178
+ - Should retry at network layer
1179
+ - Instead retried at application layer
1180
+ - Wasted compute, poor user experience
1181
+
1182
+ **Problem 4: Silent failures**
1183
+ - Multiple error types possible
1184
+ - Generic catch ignores distinctions
1185
+ - Bug: handled Error A as if it were Error B
1186
+ - Data corruption, hard to debug
1187
+
1188
+ Solutions:
1189
+
1190
+ **Tagged errors**:
1191
+ - `NetworkError`, `ValidationError`, `PermissionError`
1192
+ - Type system ensures handling
1193
+ - TypeScript compiler catches missed cases
1194
+ - Clear intent
1195
+
1196
+ **Recovery strategies**:
1197
+ - `NetworkError` → Retry with exponential backoff
1198
+ - `ValidationError` → Return user message, no retry
1199
+ - `PermissionError` → Log security event, no retry
1200
+ - `TemporaryError` → Retry with jitter
1201
+
1202
+ **Business semantics**:
1203
+ - Error type matches domain concept
1204
+ - Code reads like domain language
1205
+ - Easier to maintain
1206
+ - New developers understand quickly
1207
+
1208
+ ---
1209
+
1210
+ ---
1211
+
1212
+