@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.
- package/DOJO.md +22 -0
- package/dojo.json +50 -0
- package/katas/001-hello-effect/SENSEI.md +72 -0
- package/katas/001-hello-effect/solution.test.ts +35 -0
- package/katas/001-hello-effect/solution.ts +16 -0
- package/katas/002-transform-with-map/SENSEI.md +72 -0
- package/katas/002-transform-with-map/solution.test.ts +33 -0
- package/katas/002-transform-with-map/solution.ts +16 -0
- package/katas/003-generator-pipelines/SENSEI.md +72 -0
- package/katas/003-generator-pipelines/solution.test.ts +40 -0
- package/katas/003-generator-pipelines/solution.ts +29 -0
- package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
- package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
- package/katas/004-flatmap-and-chaining/solution.ts +18 -0
- package/katas/005-pipe-composition/SENSEI.md +81 -0
- package/katas/005-pipe-composition/solution.test.ts +41 -0
- package/katas/005-pipe-composition/solution.ts +19 -0
- package/katas/006-handle-errors/SENSEI.md +86 -0
- package/katas/006-handle-errors/solution.test.ts +53 -0
- package/katas/006-handle-errors/solution.ts +30 -0
- package/katas/007-tagged-errors/SENSEI.md +79 -0
- package/katas/007-tagged-errors/solution.test.ts +82 -0
- package/katas/007-tagged-errors/solution.ts +37 -0
- package/katas/008-error-patterns/SENSEI.md +89 -0
- package/katas/008-error-patterns/solution.test.ts +41 -0
- package/katas/008-error-patterns/solution.ts +38 -0
- package/katas/009-option-type/SENSEI.md +96 -0
- package/katas/009-option-type/solution.test.ts +49 -0
- package/katas/009-option-type/solution.ts +26 -0
- package/katas/010-either-and-exit/SENSEI.md +86 -0
- package/katas/010-either-and-exit/solution.test.ts +33 -0
- package/katas/010-either-and-exit/solution.ts +17 -0
- package/katas/011-services-and-context/SENSEI.md +82 -0
- package/katas/011-services-and-context/solution.test.ts +23 -0
- package/katas/011-services-and-context/solution.ts +17 -0
- package/katas/012-layers/SENSEI.md +73 -0
- package/katas/012-layers/solution.test.ts +23 -0
- package/katas/012-layers/solution.ts +26 -0
- package/katas/013-testing-effects/SENSEI.md +88 -0
- package/katas/013-testing-effects/solution.test.ts +41 -0
- package/katas/013-testing-effects/solution.ts +20 -0
- package/katas/014-schema-basics/SENSEI.md +81 -0
- package/katas/014-schema-basics/solution.test.ts +35 -0
- package/katas/014-schema-basics/solution.ts +25 -0
- package/katas/015-domain-modeling/SENSEI.md +85 -0
- package/katas/015-domain-modeling/solution.test.ts +46 -0
- package/katas/015-domain-modeling/solution.ts +42 -0
- package/katas/016-retry-and-schedule/SENSEI.md +72 -0
- package/katas/016-retry-and-schedule/solution.test.ts +26 -0
- package/katas/016-retry-and-schedule/solution.ts +23 -0
- package/katas/017-parallel-effects/SENSEI.md +70 -0
- package/katas/017-parallel-effects/solution.test.ts +33 -0
- package/katas/017-parallel-effects/solution.ts +17 -0
- package/katas/018-race-and-timeout/SENSEI.md +75 -0
- package/katas/018-race-and-timeout/solution.test.ts +30 -0
- package/katas/018-race-and-timeout/solution.ts +27 -0
- package/katas/019-ref-and-state/SENSEI.md +72 -0
- package/katas/019-ref-and-state/solution.test.ts +29 -0
- package/katas/019-ref-and-state/solution.ts +16 -0
- package/katas/020-fibers/SENSEI.md +80 -0
- package/katas/020-fibers/solution.test.ts +23 -0
- package/katas/020-fibers/solution.ts +23 -0
- package/katas/021-acquire-release/SENSEI.md +57 -0
- package/katas/021-acquire-release/solution.test.ts +23 -0
- package/katas/021-acquire-release/solution.ts +22 -0
- package/katas/022-scoped-layers/SENSEI.md +52 -0
- package/katas/022-scoped-layers/solution.test.ts +35 -0
- package/katas/022-scoped-layers/solution.ts +19 -0
- package/katas/023-resource-patterns/SENSEI.md +52 -0
- package/katas/023-resource-patterns/solution.test.ts +20 -0
- package/katas/023-resource-patterns/solution.ts +13 -0
- package/katas/024-streams-basics/SENSEI.md +61 -0
- package/katas/024-streams-basics/solution.test.ts +30 -0
- package/katas/024-streams-basics/solution.ts +16 -0
- package/katas/025-stream-operations/SENSEI.md +59 -0
- package/katas/025-stream-operations/solution.test.ts +26 -0
- package/katas/025-stream-operations/solution.ts +17 -0
- package/katas/026-combining-streams/SENSEI.md +54 -0
- package/katas/026-combining-streams/solution.test.ts +20 -0
- package/katas/026-combining-streams/solution.ts +16 -0
- package/katas/027-data-pipelines/SENSEI.md +58 -0
- package/katas/027-data-pipelines/solution.test.ts +22 -0
- package/katas/027-data-pipelines/solution.ts +16 -0
- package/katas/028-logging-and-spans/SENSEI.md +58 -0
- package/katas/028-logging-and-spans/solution.test.ts +50 -0
- package/katas/028-logging-and-spans/solution.ts +20 -0
- package/katas/029-http-client/SENSEI.md +59 -0
- package/katas/029-http-client/solution.test.ts +49 -0
- package/katas/029-http-client/solution.ts +24 -0
- package/katas/030-capstone/SENSEI.md +63 -0
- package/katas/030-capstone/solution.test.ts +67 -0
- package/katas/030-capstone/solution.ts +55 -0
- package/katas/031-config-and-environment/SENSEI.md +77 -0
- package/katas/031-config-and-environment/solution.test.ts +38 -0
- package/katas/031-config-and-environment/solution.ts +11 -0
- package/katas/032-cause-and-defects/SENSEI.md +90 -0
- package/katas/032-cause-and-defects/solution.test.ts +50 -0
- package/katas/032-cause-and-defects/solution.ts +23 -0
- package/katas/033-pattern-matching/SENSEI.md +86 -0
- package/katas/033-pattern-matching/solution.test.ts +36 -0
- package/katas/033-pattern-matching/solution.ts +28 -0
- package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
- package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
- package/katas/034-deferred-and-coordination/solution.ts +24 -0
- package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
- package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
- package/katas/035-queue-and-backpressure/solution.ts +21 -0
- package/katas/036-schema-advanced/SENSEI.md +81 -0
- package/katas/036-schema-advanced/solution.test.ts +55 -0
- package/katas/036-schema-advanced/solution.ts +19 -0
- package/katas/037-cache-and-memoization/SENSEI.md +73 -0
- package/katas/037-cache-and-memoization/solution.test.ts +47 -0
- package/katas/037-cache-and-memoization/solution.ts +24 -0
- package/katas/038-metrics/SENSEI.md +91 -0
- package/katas/038-metrics/solution.test.ts +39 -0
- package/katas/038-metrics/solution.ts +23 -0
- package/katas/039-managed-runtime/SENSEI.md +75 -0
- package/katas/039-managed-runtime/solution.test.ts +29 -0
- package/katas/039-managed-runtime/solution.ts +19 -0
- package/katas/040-request-batching/SENSEI.md +87 -0
- package/katas/040-request-batching/solution.test.ts +56 -0
- package/katas/040-request-batching/solution.ts +32 -0
- package/package.json +22 -0
- package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
- package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
- package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
- package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
- package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
- package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
- package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
- package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
- package/skills/effect-patterns-error-management/SKILL.md +1668 -0
- package/skills/effect-patterns-getting-started/SKILL.md +237 -0
- package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
- package/skills/effect-patterns-observability/SKILL.md +1586 -0
- package/skills/effect-patterns-platform/SKILL.md +1195 -0
- package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
- package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
- package/skills/effect-patterns-resource-management/SKILL.md +827 -0
- package/skills/effect-patterns-scheduling/SKILL.md +451 -0
- package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
- package/skills/effect-patterns-streams/SKILL.md +2052 -0
- package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
- package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
- package/skills/effect-patterns-testing/SKILL.md +1632 -0
- package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
- package/skills/effect-patterns-value-handling/SKILL.md +676 -0
- package/tsconfig.json +20 -0
- 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
|
+
|