opencode-effect-enforcer 0.2.0

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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,1581 @@
1
+ ---
2
+ name: effect-error-handling
3
+ description: Implement typed error handling in Effect v4 using Schema.TaggedError, catchTag/catchTags, catchReason/catchReasons, Cause, ErrorReporter, and recovery patterns. Use this skill when working with Effect error channels, handling expected failures, or designing error recovery strategies.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in typed error handling, recovery patterns, and error channel management in **Effect v4**.
7
+
8
+ ## Effect Source Reference
9
+
10
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
11
+ Browse and read files there directly to look up APIs, types, and implementations.
12
+
13
+ Reference this for:
14
+
15
+ - Schema.TaggedError and error class creation
16
+ - Error handling combinators (catchTag, catchTags, catch, catchReason, catchReasons)
17
+ - Error transformation and recovery patterns
18
+ - Cause structure and inspection
19
+ - ErrorReporter module
20
+ - Defects vs error channel distinction
21
+
22
+ ## v3 to v4 Error API Changes
23
+
24
+ **This table is authoritative. Never use the v3 names.**
25
+
26
+ ### Effect Catch Combinators
27
+
28
+ | v3 (DO NOT USE) | v4 (USE THIS) | Notes |
29
+ | --------------------------- | --------------------------- | -------------------------------------------------- |
30
+ | `Effect.catchAll` | `Effect.catch` | Renamed |
31
+ | `Effect.catchAllCause` | `Effect.catchCause` | Renamed |
32
+ | `Effect.catchAllDefect` | `Effect.catchDefect` | Renamed |
33
+ | `Effect.catchSome` | `Effect.catchFilter` | Uses `Filter` module instead of `Option` |
34
+ | `Effect.catchSomeCause` | `Effect.catchCauseFilter` | Uses `Filter` module instead of `Option` |
35
+ | `Effect.catchSomeDefect` | Removed | No replacement |
36
+ | `Effect.optionFromOptional` | `Effect.catchNoSuchElement` | Renamed |
37
+ | `Effect.catchTag` | `Effect.catchTag` | Enhanced: accepts array of tags, optional `orElse` |
38
+ | `Effect.catchTags` | `Effect.catchTags` | Enhanced: optional `orElse` fallback |
39
+ | `Effect.catchIf` | `Effect.catchIf` | Enhanced: optional `orElse` fallback |
40
+ | (none) | `Effect.catchReason` | NEW: catch nested reason within tagged error |
41
+ | (none) | `Effect.catchReasons` | NEW: catch multiple nested reasons |
42
+ | (none) | `Effect.unwrapReason` | NEW: promote nested reasons to error channel |
43
+ | (none) | `Effect.catchEager` | NEW: synchronous recovery optimization |
44
+ | (none) | `Effect.withErrorReporting` | NEW: report errors to registered ErrorReporters |
45
+
46
+ ### Cause Structure
47
+
48
+ | v3 (DO NOT USE) | v4 (USE THIS) | Notes |
49
+ | -------------------------------- | ------------------------------------------ | ----------------------------- |
50
+ | 6-variant recursive tree | `{ reasons: ReadonlyArray<Reason<E>> }` | Flattened |
51
+ | `Cause.sequential(l, r)` | `Cause.combine(l, r)` | Concatenates reasons arrays |
52
+ | `Cause.parallel(l, r)` | `Cause.combine(l, r)` | Same as sequential |
53
+ | `Cause.isFailType(cause)` | `Cause.isFailReason(reason)` | Operates on Reason, not Cause |
54
+ | `Cause.isDieType(cause)` | `Cause.isDieReason(reason)` | Operates on Reason, not Cause |
55
+ | `Cause.isInterruptType(cause)` | `Cause.isInterruptReason(reason)` | Operates on Reason, not Cause |
56
+ | `Cause.isFailure(cause)` | `Cause.hasFails(cause)` | Renamed |
57
+ | `Cause.isDie(cause)` | `Cause.hasDies(cause)` | Renamed |
58
+ | `Cause.isInterrupted(cause)` | `Cause.hasInterrupts(cause)` | Renamed |
59
+ | `Cause.isInterruptedOnly(cause)` | `Cause.hasInterruptsOnly(cause)` | Renamed |
60
+ | `Cause.failureOption(cause)` | `Cause.findErrorOption(cause)` | Renamed |
61
+ | `Cause.failureOrCause(cause)` | `Cause.findError(cause)` | Returns `Result.Result` now |
62
+ | `Cause.dieOption(cause)` | `Cause.findDefect(cause)` | Returns `Result.Result` now |
63
+ | `Cause.interruptOption(cause)` | `Cause.findInterrupt(cause)` | Returns `Result.Result` now |
64
+ | `Cause.failures(cause)` | `cause.reasons.filter(Cause.isFailReason)` | Use array filter |
65
+ | `Cause.defects(cause)` | `cause.reasons.filter(Cause.isDieReason)` | Use array filter |
66
+
67
+ ### Error Class Renames (`*Exception` to `*Error`)
68
+
69
+ | v3 (DO NOT USE) | v4 (USE THIS) |
70
+ | -------------------------------------- | ----------------------------- |
71
+ | `Cause.NoSuchElementException` | `Cause.NoSuchElementError` |
72
+ | `Cause.TimeoutException` | `Cause.TimeoutError` |
73
+ | `Cause.IllegalArgumentException` | `Cause.IllegalArgumentError` |
74
+ | `Cause.ExceededCapacityException` | `Cause.ExceededCapacityError` |
75
+ | `Cause.UnknownException` | `Cause.UnknownError` |
76
+ | `Cause.RuntimeException` | Removed |
77
+ | `Cause.InterruptedException` | Removed |
78
+ | `Cause.InvalidPubSubCapacityException` | Removed |
79
+
80
+ ### Schema Error Renames
81
+
82
+ | Old API (DO NOT USE) | Effect v4 API (USE THIS) |
83
+ | --------------------------------- | ------------------------------ |
84
+ | `Schema.TaggedErrorClass` | `Schema.TaggedError` |
85
+ | `Schema.ErrorClass` | `Schema.Error` |
86
+ | `Schema.Error` (instance schema) | `Schema.ErrorInstance` |
87
+ | `Schema.ErrorReviver` | `Schema.ErrorInstanceReviver` |
88
+ | `ParseError` | `Schema.SchemaError` |
89
+
90
+ ## Core Error Handling Philosophy
91
+
92
+ Effect distinguishes between two types of failures:
93
+
94
+ 1. **Expected Errors (Error Channel)** - Business logic failures that should be handled
95
+ - Type-safe and tracked in the effect signature: `Effect<A, E, R>`
96
+ - Represented by the `E` type parameter
97
+ - Handle with catchTag, catchTags, catch, catchReason, catchReasons
98
+
99
+ 2. **Unexpected Errors (Defects)** - Programming errors that indicate bugs
100
+ - Not tracked in the type system
101
+ - Result from programming mistakes (null refs, unhandled cases, assertions)
102
+ - Usually should NOT be caught; use catchDefect only at boundaries
103
+
104
+ ### Runtime Adapter Boundaries and Invariants
105
+
106
+ Do not force every impossible or adapter-internal failure into a tagged error just to satisfy a blanket rule.
107
+
108
+ Use typed errors for:
109
+
110
+ - caller-actionable failures
111
+ - business or protocol failures that the next layer can recover from
112
+ - public service contracts
113
+
114
+ Use defects or `Effect.orDie` for:
115
+
116
+ - impossible branches and invariant violations
117
+ - runtime-adapter internals where no caller can recover meaningfully
118
+ - collapsing noisy upstream error surfaces at a boundary that should not leak them further
119
+
120
+ `new Error(...)` is acceptable inside `Effect.die(...)`, invariant branches, or adapter-only defect paths. It is not acceptable as the public error model for recoverable domain behavior.
121
+
122
+ ### When to Use Error Channel vs Defects
123
+
124
+ ```typescript
125
+ import * as Effect from 'effect/Effect';
126
+ import * as Schema from 'effect/Schema';
127
+
128
+ declare const findUser: (userId: string) => Effect.Effect<User, UserNotFound>;
129
+ declare const validatePassword: (
130
+ user: User,
131
+ password: string
132
+ ) => Effect.Effect<boolean, InvalidCredentials>;
133
+ declare const database: {
134
+ query: (
135
+ sql: string,
136
+ ...params: ReadonlyArray<unknown>
137
+ ) => Effect.Effect<unknown>;
138
+ };
139
+
140
+ interface User {
141
+ readonly id: string;
142
+ readonly name: string;
143
+ }
144
+
145
+ // CORRECT - Expected business failures in error channel
146
+ class UserNotFound extends Schema.TaggedError<UserNotFound>()(
147
+ 'UserNotFound',
148
+ {
149
+ userId: Schema.String,
150
+ message: Schema.String
151
+ }
152
+ ) {}
153
+
154
+ class InvalidCredentials extends Schema.TaggedError<InvalidCredentials>()(
155
+ 'InvalidCredentials',
156
+ { reason: Schema.String, message: Schema.String }
157
+ ) {}
158
+
159
+ const authenticateUser = (
160
+ userId: string,
161
+ password: string
162
+ ): Effect.Effect<User, UserNotFound | InvalidCredentials> =>
163
+ Effect.gen(function* () {
164
+ const user = yield* findUser(userId); // Can fail with UserNotFound
165
+ const valid = yield* validatePassword(user, password); // Can fail with InvalidCredentials
166
+ return user;
167
+ });
168
+
169
+ // CORRECT - Programmer errors as defects (use Effect.die)
170
+ const assertPositive = (n: number): Effect.Effect<number> =>
171
+ n > 0
172
+ ? Effect.succeed(n)
173
+ : Effect.die(new Error(`Expected positive number, got ${n}`));
174
+
175
+ // WRONG - Business failure as defect
176
+ const findUserWrong = (userId: string): Effect.Effect<User> =>
177
+ Effect.gen(function* () {
178
+ const user = yield* database.query(
179
+ 'SELECT * FROM users WHERE id = ?',
180
+ userId
181
+ );
182
+ if (!user) {
183
+ yield* Effect.die(new Error('User not found')); // Should be in error channel!
184
+ }
185
+ return user as User;
186
+ });
187
+ ```
188
+
189
+ ## Error Class Decision Tree
190
+
191
+ Effect v4 provides three ways to define error classes. Choose based on context:
192
+
193
+ ### `Schema.TaggedError` — Primary choice for domain errors
194
+
195
+ Schema-validated, automatically tagged with `_tag`, catchable via `catchTag`. Use for all cross-module and public API errors.
196
+
197
+ ```typescript
198
+ import * as Schema from 'effect/Schema';
199
+
200
+ class NotFound extends Schema.TaggedError<NotFound>()(
201
+ 'NotFound',
202
+ { id: Schema.String, message: Schema.String },
203
+ { description: 'Entity was not found.' }
204
+ ) {}
205
+
206
+ // Constructed with schema validation
207
+ const error = new NotFound({ id: '123', message: 'User not found' });
208
+ error._tag; // "NotFound"
209
+ ```
210
+
211
+ ### `Schema.Error` — For manual tag control
212
+
213
+ Schema-validated but no automatic `_tag`. Use when you need a custom discriminator field (e.g., HttpApiError types use `_tag: Schema.tag("NotFound")` manually).
214
+
215
+ ```typescript
216
+ import * as Schema from 'effect/Schema';
217
+
218
+ class NotFound extends Schema.Error<NotFound>('NotFound')({
219
+ _tag: Schema.tag('NotFound'),
220
+ message: Schema.String
221
+ }) {}
222
+ ```
223
+
224
+ ### `Data.TaggedError` — Lightweight, no schema validation
225
+
226
+ No schema validation overhead. Use for module-internal errors or hot paths where schema decoding cost is unwanted.
227
+
228
+ ```typescript
229
+ import * as Data from 'effect/Data';
230
+
231
+ class InternalError extends Data.TaggedError('InternalError')<{
232
+ readonly message: string;
233
+ }> {}
234
+
235
+ // Still catchable via catchTag
236
+ const program = Effect.fail(new InternalError({ message: 'oops' })).pipe(
237
+ Effect.catchTag('InternalError', (e) => Effect.succeed(e.message))
238
+ );
239
+ ```
240
+
241
+ ### Decision summary
242
+
243
+ | Scenario | Use |
244
+ | ------------------------------------------- | ------------------------- |
245
+ | Cross-module / public API errors | `Schema.TaggedError` |
246
+ | Errors that need `httpApiStatus` annotation | `Schema.TaggedError` |
247
+ | Errors with a `reason` union field | `Schema.TaggedError` |
248
+ | Custom discriminator field (not `_tag`) | `Schema.Error` |
249
+ | Module-internal, no serialization needed | `Data.TaggedError` |
250
+
251
+ ## Creating Tagged Errors
252
+
253
+ Always use `Schema.TaggedError` for domain errors with a `message` field.
254
+
255
+ ### Basic Tagged Error
256
+
257
+ ```typescript
258
+ import * as Schema from 'effect/Schema';
259
+
260
+ // Simple error with message only
261
+ export class NetworkError extends Schema.TaggedError<NetworkError>()(
262
+ 'NetworkError',
263
+ { message: Schema.String },
264
+ { description: 'Network request failed.' }
265
+ ) {}
266
+
267
+ // Error with rich context
268
+ export class ValidationError extends Schema.TaggedError<ValidationError>()(
269
+ 'ValidationError',
270
+ {
271
+ field: Schema.String,
272
+ message: Schema.String,
273
+ value: Schema.optional(Schema.Unknown)
274
+ },
275
+ { description: 'Input validation failed for a specific field.' }
276
+ ) {}
277
+
278
+ // Usage
279
+ const error = new ValidationError({
280
+ field: 'email',
281
+ message: 'Invalid email format',
282
+ value: 'not-an-email'
283
+ });
284
+ ```
285
+
286
+ ### Error with Reason Discriminator
287
+
288
+ For bindings that wrap a single external system, use a `reason` literal union to keep the error surface compact while remaining precise:
289
+
290
+ ```typescript
291
+ import * as Schema from 'effect/Schema';
292
+
293
+ export class ApiError extends Schema.TaggedError<ApiError>()(
294
+ 'ApiError',
295
+ {
296
+ reason: Schema.Literals([
297
+ 'BadRequest',
298
+ 'Unauthorized',
299
+ 'NotFound',
300
+ 'RateLimited',
301
+ 'ServerError',
302
+ 'Timeout'
303
+ ]),
304
+ message: Schema.String,
305
+ statusCode: Schema.optional(Schema.Number),
306
+ details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
307
+ },
308
+ { description: 'Failure from an external API operation.' }
309
+ ) {}
310
+ ```
311
+
312
+ ### Error with Nested Reason Types (for `catchReason`/`catchReasons`)
313
+
314
+ When reason variants carry distinct payloads, model each as a separate `TaggedError` and compose with `Schema.Union`. This enables v4's `catchReason` and `catchReasons`:
315
+
316
+ ```typescript
317
+ import * as Schema from 'effect/Schema';
318
+
319
+ export class RateLimitError extends Schema.TaggedError<RateLimitError>()(
320
+ 'RateLimitError',
321
+ { retryAfter: Schema.Number },
322
+ { description: 'Rate limit exceeded.' }
323
+ ) {}
324
+
325
+ export class QuotaExceededError extends Schema.TaggedError<QuotaExceededError>()(
326
+ 'QuotaExceededError',
327
+ { limit: Schema.Number },
328
+ { description: 'Quota exhausted.' }
329
+ ) {}
330
+
331
+ export class SafetyBlockedError extends Schema.TaggedError<SafetyBlockedError>()(
332
+ 'SafetyBlockedError',
333
+ { category: Schema.String },
334
+ { description: 'Blocked by safety filter.' }
335
+ ) {}
336
+
337
+ export class AiError extends Schema.TaggedError<AiError>()(
338
+ 'AiError',
339
+ {
340
+ reason: Schema.Union([
341
+ RateLimitError,
342
+ QuotaExceededError,
343
+ SafetyBlockedError
344
+ ])
345
+ },
346
+ { description: 'Failure from an AI model call.' }
347
+ ) {}
348
+ ```
349
+
350
+ ### Error with HTTP Status Annotation
351
+
352
+ For errors that map to HTTP responses, use the `httpApiStatus` annotation:
353
+
354
+ ```typescript
355
+ import * as Schema from 'effect/Schema';
356
+
357
+ export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
358
+ 'Unauthorized',
359
+ { message: Schema.String },
360
+ {
361
+ httpApiStatus: 401,
362
+ description: 'Request lacks valid authentication credentials.'
363
+ }
364
+ ) {}
365
+
366
+ export class EntityNotFound extends Schema.TaggedError<EntityNotFound>()(
367
+ 'EntityNotFound',
368
+ { entityType: Schema.String, id: Schema.String, message: Schema.String },
369
+ {
370
+ httpApiStatus: 404,
371
+ description: 'Requested entity does not exist.'
372
+ }
373
+ ) {}
374
+ ```
375
+
376
+ ### Error with Custom Properties
377
+
378
+ ```typescript
379
+ import * as Schema from 'effect/Schema';
380
+
381
+ export class HttpError extends Schema.TaggedError<HttpError>()(
382
+ 'HttpError',
383
+ {
384
+ status: Schema.Number,
385
+ body: Schema.String,
386
+ message: Schema.String
387
+ },
388
+ { description: 'HTTP response error.' }
389
+ ) {
390
+ get isClientError() {
391
+ return this.status >= 400 && this.status < 500;
392
+ }
393
+
394
+ get isServerError() {
395
+ return this.status >= 500;
396
+ }
397
+ }
398
+ ```
399
+
400
+ ## Error Wrapping Conventions
401
+
402
+ When wrapping upstream errors in domain error classes, choose the `cause` field schema based on intent:
403
+
404
+ | `cause` Schema | When to use | Example |
405
+ | ---------------- | -------------------------------------------------------------- | -------------------------------- |
406
+ | `Schema.Defect()` | Wrapping unknown/untyped upstream errors (throwables, defects) | `DevToolsError`, `DatabaseError` |
407
+ | `Schema.Unknown` | Preserving full upstream error structure for debugging | `SubstackFetchError` |
408
+ | `Schema.String` | Message-only wrapping where structure is irrelevant | `AuthError` |
409
+ | Omitted | When the error tag + fields fully describe the failure | `UserNotFound`, `Unauthorized` |
410
+
411
+ ```typescript
412
+ import * as Schema from 'effect/Schema';
413
+
414
+ // Defect-style: wraps throwables and unknown failures
415
+ export class DatabaseError extends Schema.TaggedError<DatabaseError>()(
416
+ 'DatabaseError',
417
+ {
418
+ operation: Schema.String,
419
+ message: Schema.String,
420
+ cause: Schema.Defect()
421
+ },
422
+ { description: 'Database operation failed.' }
423
+ ) {}
424
+
425
+ // Unknown-style: preserves full upstream error
426
+ export class FetchError extends Schema.TaggedError<FetchError>()(
427
+ 'FetchError',
428
+ {
429
+ url: Schema.String,
430
+ message: Schema.String,
431
+ cause: Schema.Unknown
432
+ },
433
+ { description: 'HTTP fetch operation failed.' }
434
+ ) {}
435
+
436
+ // String-style: message-only wrapper
437
+ export class AuthError extends Schema.TaggedError<AuthError>()(
438
+ 'AuthError',
439
+ {
440
+ cause: Schema.String
441
+ },
442
+ { description: 'Authentication backend failure.' }
443
+ ) {}
444
+ ```
445
+
446
+ ## Yieldable Errors
447
+
448
+ In `Effect.gen` blocks, tagged error instances can be yielded directly as a shorthand for `yield* Effect.fail(...)`. This works because `Schema.TaggedError`, `Schema.Error`, and `Data.TaggedError` all extend `Cause.YieldableError`.
449
+
450
+ ```typescript
451
+ import { Effect } from 'effect';
452
+ import * as Schema from 'effect/Schema';
453
+
454
+ class NotFound extends Schema.TaggedError<NotFound>()('NotFound', {
455
+ id: Schema.String,
456
+ message: Schema.String
457
+ }) {}
458
+
459
+ // These two are equivalent:
460
+ const explicit = Effect.gen(function* () {
461
+ return yield* Effect.fail(
462
+ new NotFound({ id: '123', message: 'User not found' })
463
+ );
464
+ });
465
+
466
+ const shorthand = Effect.gen(function* () {
467
+ return yield* new NotFound({ id: '123', message: 'User not found' });
468
+ });
469
+ ```
470
+
471
+ The shorthand form is idiomatic and preferred in Effect v4 generators. It reads naturally as "yield this error" and reduces noise.
472
+
473
+ ## Handling Errors by Tag
474
+
475
+ ### catchTag - Single Error Type
476
+
477
+ ```typescript
478
+ import * as Effect from 'effect/Effect';
479
+ import * as Schema from 'effect/Schema';
480
+
481
+ declare const createGuestUser: (id: string) => User;
482
+
483
+ interface User {
484
+ readonly id: string;
485
+ readonly name: string;
486
+ }
487
+
488
+ class NotFound extends Schema.TaggedError<NotFound>()('NotFound', {
489
+ id: Schema.String,
490
+ message: Schema.String
491
+ }) {}
492
+
493
+ class Unauthorized extends Schema.TaggedError<Unauthorized>()(
494
+ 'Unauthorized',
495
+ {
496
+ message: Schema.String
497
+ }
498
+ ) {}
499
+
500
+ // Effect<User, NotFound | Unauthorized, Dependencies>
501
+ // v
502
+ const getUser = (id: string): Effect.Effect<User, NotFound | Unauthorized> =>
503
+ Effect.fail(new NotFound({ id, message: `User ${id} not found` }));
504
+
505
+ // Handle single error type
506
+ // Effect<User, Unauthorized, Dependencies>
507
+ // v
508
+ const program = getUser('123').pipe(
509
+ Effect.catchTag('NotFound', (error) =>
510
+ // Return default user when not found
511
+ Effect.succeed(createGuestUser(error.id))
512
+ )
513
+ );
514
+ ```
515
+
516
+ ### catchTag - Array Form (v4)
517
+
518
+ In Effect v4, `catchTag` accepts an array of tags to handle multiple error types with a single handler:
519
+
520
+ ```typescript
521
+ import { Effect, Schema } from 'effect';
522
+
523
+ class ParseError extends Schema.TaggedError<ParseError>()('ParseError', {
524
+ input: Schema.String,
525
+ message: Schema.String
526
+ }) {}
527
+
528
+ class ReservedPortError extends Schema.TaggedError<ReservedPortError>()(
529
+ 'ReservedPortError',
530
+ {
531
+ port: Schema.Number
532
+ }
533
+ ) {}
534
+
535
+ declare const loadPort: (
536
+ input: string
537
+ ) => Effect.Effect<number, ParseError | ReservedPortError>;
538
+
539
+ // Catch multiple tags with one handler - the error is typed as the union
540
+ const program = loadPort('80').pipe(
541
+ Effect.catchTag(['ParseError', 'ReservedPortError'], (_) =>
542
+ Effect.succeed(3000)
543
+ )
544
+ );
545
+ ```
546
+
547
+ ### catchTag / catchTags - Optional `orElse` Fallback (v4)
548
+
549
+ In v4, `catchTag`, `catchTags`, and `catchIf` accept an optional trailing `orElse` parameter for unmatched errors:
550
+
551
+ ```typescript
552
+ import { Effect, Schema } from 'effect';
553
+
554
+ class NotFound extends Schema.TaggedError<NotFound>()('NotFound', {
555
+ message: Schema.String
556
+ }) {}
557
+
558
+ class Forbidden extends Schema.TaggedError<Forbidden>()('Forbidden', {
559
+ message: Schema.String
560
+ }) {}
561
+
562
+ class ServerError extends Schema.TaggedError<ServerError>()(
563
+ 'ServerError',
564
+ {
565
+ message: Schema.String
566
+ }
567
+ ) {}
568
+
569
+ declare const riskyOp: () => Effect.Effect<
570
+ string,
571
+ NotFound | Forbidden | ServerError
572
+ >;
573
+
574
+ // The third argument is the orElse handler for unmatched errors
575
+ const program = riskyOp().pipe(
576
+ Effect.catchTag(
577
+ 'NotFound',
578
+ (e) => Effect.succeed('default'),
579
+ (unmatched) => Effect.die(unmatched) // Forbidden | ServerError
580
+ )
581
+ );
582
+
583
+ // Works with catchTags too
584
+ const program2 = riskyOp().pipe(
585
+ Effect.catchTags(
586
+ {
587
+ NotFound: (e) => Effect.succeed('default'),
588
+ Forbidden: (e) => Effect.succeed('forbidden')
589
+ },
590
+ (unmatched) => Effect.die(unmatched) // ServerError
591
+ )
592
+ );
593
+ ```
594
+
595
+ > **Type preservation (beta.71):** When you omit `orElse`, the tags you do not handle stay in the error channel — `catchTag(['NotFound'], ...)` on `Effect<string, NotFound | Forbidden | ServerError>` yields `Effect<string, Forbidden | ServerError>`. Supplying `orElse` handles those remaining variants, so the resulting error channel reflects only what the fallback produces. A beta.71 fix ensures `catchTag` / `catchTags` / `catchIf` no longer silently drop the unhandled error types from the inferred type.
596
+
597
+ ### catchTags - Multiple Error Types
598
+
599
+ ```typescript
600
+ import * as Effect from 'effect/Effect';
601
+ import * as Schema from 'effect/Schema';
602
+
603
+ interface Data {
604
+ readonly data: ReadonlyArray<unknown>;
605
+ readonly cached?: boolean;
606
+ readonly timeout?: boolean;
607
+ readonly parseError?: boolean;
608
+ }
609
+
610
+ class NetworkError extends Schema.TaggedError<NetworkError>()(
611
+ 'NetworkError',
612
+ {
613
+ message: Schema.String
614
+ }
615
+ ) {}
616
+
617
+ class TimeoutError extends Schema.TaggedError<TimeoutError>()(
618
+ 'TimeoutError',
619
+ {
620
+ message: Schema.String
621
+ }
622
+ ) {}
623
+
624
+ class ParseError extends Schema.TaggedError<ParseError>()('ParseError', {
625
+ input: Schema.String,
626
+ message: Schema.String
627
+ }) {}
628
+
629
+ // Effect<Data, NetworkError | TimeoutError | ParseError, Dependencies>
630
+ // v
631
+ const fetchData = (): Effect.Effect<
632
+ Data,
633
+ NetworkError | TimeoutError | ParseError
634
+ > => Effect.fail(new NetworkError({ message: 'Connection refused' }));
635
+
636
+ // Handle multiple error types at once
637
+ // Effect<Data, never, Dependencies>
638
+ // v
639
+ const program = fetchData().pipe(
640
+ Effect.catchTags({
641
+ NetworkError: (_error) => Effect.succeed({ data: [], cached: true }),
642
+
643
+ TimeoutError: (_error) => Effect.succeed({ data: [], timeout: true }),
644
+
645
+ ParseError: (error) =>
646
+ // Access error-specific fields
647
+ Effect.logError(`Failed to parse: ${error.input}`).pipe(
648
+ Effect.as({ data: [], parseError: true })
649
+ )
650
+ })
651
+ );
652
+ ```
653
+
654
+ ### catch - Handle All Errors
655
+
656
+ `Effect.catch` (renamed from `catchAll` in v3) handles all errors with a single handler:
657
+
658
+ ```typescript
659
+ import * as Effect from 'effect/Effect';
660
+ import * as Schema from 'effect/Schema';
661
+
662
+ declare const getDefaultResult: () => Result;
663
+
664
+ interface Result {
665
+ readonly value: string;
666
+ }
667
+
668
+ class InvalidInput extends Schema.TaggedError<InvalidInput>()(
669
+ 'InvalidInput',
670
+ {
671
+ message: Schema.String
672
+ }
673
+ ) {}
674
+
675
+ class ProcessingError extends Schema.TaggedError<ProcessingError>()(
676
+ 'ProcessingError',
677
+ {
678
+ message: Schema.String
679
+ }
680
+ ) {}
681
+
682
+ const process = (): Effect.Effect<Result, InvalidInput | ProcessingError> =>
683
+ Effect.fail(new InvalidInput({ message: 'Bad input' }));
684
+
685
+ const program = process().pipe(
686
+ Effect.catch((error) =>
687
+ // error is typed as: InvalidInput | ProcessingError
688
+ Effect.logError(`Operation failed: ${error._tag}`).pipe(
689
+ Effect.as(getDefaultResult())
690
+ )
691
+ )
692
+ );
693
+ ```
694
+
695
+ **Migration note:** When replacing `Effect.promise(() => Service.method())` with direct service yields (`yield* service.method()`), errors that previously flowed as defects (untyped Promise rejections) become typed channel errors. Existing `catchDefect` handlers must be replaced with `catch` or `catchTag` to match the now-typed error channel.
696
+
697
+ ### catchEager - Synchronous Recovery (v4)
698
+
699
+ `Effect.catchEager` is an optimization of `catch` that evaluates synchronous recovery effects immediately rather than suspending. Use for lightweight wrapping where the recovery handler is always synchronous:
700
+
701
+ ```typescript
702
+ import { Effect, Schema } from 'effect';
703
+
704
+ class CliConfigError extends Schema.TaggedError<CliConfigError>()(
705
+ 'CliConfigError',
706
+ {
707
+ message: Schema.String
708
+ }
709
+ ) {}
710
+
711
+ const loadCredentials = Effect.tryPromise({
712
+ try: () => readFile('~/.config/myapp/credentials.json'),
713
+ catch: (cause) => cause
714
+ }).pipe(
715
+ Effect.catchEager((cause) =>
716
+ Effect.fail(
717
+ new CliConfigError({
718
+ message: `Failed to load credentials: ${cause}`
719
+ })
720
+ )
721
+ )
722
+ );
723
+ ```
724
+
725
+ ### catchNoSuchElement - Convert NoSuchElementError to Option (v4)
726
+
727
+ `Effect.catchNoSuchElement` (renamed from `optionFromOptional` in v3) catches `Cause.NoSuchElementError` and converts the result to `Option`:
728
+
729
+ ```typescript
730
+ import { Effect } from 'effect';
731
+ import * as Option from 'effect/Option';
732
+
733
+ declare const maybeFindItem: () => Effect.Effect<
734
+ string,
735
+ Cause.NoSuchElementError
736
+ >;
737
+
738
+ // Effect<Option<string>, never>
739
+ const program = Effect.catchNoSuchElement(maybeFindItem());
740
+ ```
741
+
742
+ ## Handling Nested Error Reasons (v4)
743
+
744
+ When errors use a `reason` field containing a tagged union (see "Error with Nested Reason Types" above), v4 provides three purpose-built APIs.
745
+
746
+ ### catchReason - Catch One Specific Reason
747
+
748
+ Catches a specific `reason` variant within a tagged error without removing the parent error from the error channel:
749
+
750
+ ```typescript
751
+ import { Effect } from 'effect';
752
+
753
+ declare const callModel: Effect.Effect<string, AiError>;
754
+
755
+ // Catch only RateLimitError reason within AiError
756
+ const program = callModel.pipe(
757
+ Effect.catchReason(
758
+ 'AiError', // parent error _tag
759
+ 'RateLimitError', // reason _tag to catch
760
+ (reason) => Effect.succeed(`Retry after ${reason.retryAfter} seconds`)
761
+ )
762
+ );
763
+
764
+ // With optional orElse for uncaught reasons
765
+ const withFallback = callModel.pipe(
766
+ Effect.catchReason(
767
+ 'AiError',
768
+ 'RateLimitError',
769
+ (reason) => Effect.succeed(`Retry after ${reason.retryAfter} seconds`),
770
+ (reason) => Effect.succeed(`Model call failed: ${reason._tag}`) // QuotaExceeded | SafetyBlocked
771
+ )
772
+ );
773
+ ```
774
+
775
+ ### catchReasons - Catch Multiple Reasons
776
+
777
+ Handle multiple reason variants at once via an object of handlers:
778
+
779
+ ```typescript
780
+ import { Effect } from 'effect';
781
+
782
+ declare const callModel: Effect.Effect<string, AiError>;
783
+
784
+ const program = callModel.pipe(
785
+ Effect.catchReasons('AiError', {
786
+ RateLimitError: (reason) =>
787
+ Effect.succeed(`Retry after ${reason.retryAfter} seconds`),
788
+ QuotaExceededError: (reason) =>
789
+ Effect.succeed(`Quota exceeded at ${reason.limit} tokens`)
790
+ })
791
+ // SafetyBlockedError remains unhandled in the error channel
792
+ );
793
+ ```
794
+
795
+ ### unwrapReason - Promote Reasons to Error Channel
796
+
797
+ Unwraps the `reason` field, replacing the parent error with its reason variants in the error channel. Useful when you want to `catchTags` the individual reasons directly:
798
+
799
+ ```typescript
800
+ import { Effect } from 'effect';
801
+
802
+ declare const callModel: Effect.Effect<string, AiError>;
803
+
804
+ const program = callModel.pipe(
805
+ Effect.unwrapReason('AiError'),
806
+ // Error channel is now: RateLimitError | QuotaExceededError | SafetyBlockedError
807
+ Effect.catchTags({
808
+ RateLimitError: (r) => Effect.succeed(`Back off for ${r.retryAfter}s`),
809
+ QuotaExceededError: (r) =>
810
+ Effect.succeed(`Increase quota beyond ${r.limit}`),
811
+ SafetyBlockedError: (r) => Effect.succeed(`Blocked: ${r.category}`)
812
+ })
813
+ );
814
+ ```
815
+
816
+ ## Catching by Filter (v4)
817
+
818
+ `Effect.catchFilter` replaces v3's `Effect.catchSome` (which used `Option`). It uses the `Filter` module:
819
+
820
+ ```typescript
821
+ import { Effect, Filter } from 'effect';
822
+
823
+ // v3 (DO NOT USE):
824
+ // Effect.catchSome((error) =>
825
+ // error === 42 ? Option.some(Effect.succeed("caught")) : Option.none()
826
+ // )
827
+
828
+ // v4:
829
+ const program = Effect.fail(42).pipe(
830
+ Effect.catchFilter(
831
+ Filter.fromPredicate((error: number) => error === 42),
832
+ (error) => Effect.succeed('caught')
833
+ )
834
+ );
835
+ ```
836
+
837
+ `Effect.catchCauseFilter` is the Cause-level equivalent (replaces `catchSomeCause`).
838
+
839
+ ## Cause Structure (v4)
840
+
841
+ In v4, `Cause<E>` is a flat wrapper around an array of reasons — **not** a recursive tree.
842
+
843
+ ```typescript
844
+ interface Cause<E> {
845
+ readonly reasons: ReadonlyArray<Reason<E>>;
846
+ }
847
+
848
+ type Reason<E> = Fail<E> | Die | Interrupt;
849
+ ```
850
+
851
+ There are only three reason variants:
852
+
853
+ - `Fail<E>` — `{ readonly error: E }` — expected typed failures
854
+ - `Die` — `{ readonly defect: unknown }` — unexpected defects
855
+ - `Interrupt` — `{ readonly fiberId: number | undefined }` — fiber interruptions
856
+
857
+ An empty cause is `cause.reasons.length === 0`. The `Empty`, `Sequential`, and `Parallel` variants from v3 no longer exist.
858
+
859
+ ### Inspecting Causes
860
+
861
+ ```typescript
862
+ import { Cause } from 'effect';
863
+
864
+ const inspectCause = <E>(cause: Cause.Cause<E>) => {
865
+ // Iterate over the flat reasons array
866
+ for (const reason of cause.reasons) {
867
+ if (Cause.isFailReason(reason)) {
868
+ console.log('Expected error:', reason.error);
869
+ } else if (Cause.isDieReason(reason)) {
870
+ console.log('Defect:', reason.defect);
871
+ } else if (Cause.isInterruptReason(reason)) {
872
+ console.log('Interrupted by fiber:', reason.fiberId);
873
+ }
874
+ }
875
+ };
876
+ ```
877
+
878
+ ### Cause Extractors
879
+
880
+ ```typescript
881
+ import { Cause } from 'effect';
882
+ import * as Option from 'effect/Option';
883
+
884
+ declare const cause: Cause.Cause<string>;
885
+
886
+ // Extract first error as Option
887
+ const errorOpt: Option.Option<string> = Cause.findErrorOption(cause);
888
+
889
+ // Extract first error as Result (Result.Result<E, Cause<never>>)
890
+ const errorResult = Cause.findError(cause);
891
+
892
+ // Extract first defect as Result
893
+ const defectResult = Cause.findDefect(cause);
894
+
895
+ // Check what a cause contains
896
+ Cause.hasFails(cause); // has at least one Fail reason
897
+ Cause.hasDies(cause); // has at least one Die reason
898
+ Cause.hasInterrupts(cause); // has at least one Interrupt reason
899
+ Cause.hasInterruptsOnly(cause); // only Interrupt reasons, no Fail/Die
900
+
901
+ // Human-readable rendering
902
+ const pretty: string = Cause.pretty(cause);
903
+ ```
904
+
905
+ ### Cause Constructors (v4)
906
+
907
+ ```typescript
908
+ import { Cause } from 'effect';
909
+
910
+ Cause.empty; // empty cause (no reasons)
911
+ Cause.fail(error); // single Fail reason
912
+ Cause.die(defect); // single Die reason
913
+ Cause.interrupt(fiberId); // single Interrupt reason
914
+ Cause.combine(left, right); // concatenate two causes' reasons
915
+ Cause.fromReasons(reasons); // construct from array of Reason values
916
+ Cause.makeFailReason(error); // construct a Fail reason
917
+ Cause.makeDieReason(defect); // construct a Die reason
918
+ Cause.makeInterruptReason(fiberId); // construct an Interrupt reason
919
+ Cause.annotate(cause, annotations); // attach metadata
920
+ ```
921
+
922
+ ### Cause.Done - Graceful Completion Signal (v4)
923
+
924
+ `Cause.Done<A>` is a graceful completion value used by queues, pulls, and streams. It travels through the typed error channel, but consumers interpret it as successful end-of-input rather than an operational failure; its `value` can carry a final leftover payload.
925
+
926
+ ```typescript
927
+ import { Cause } from 'effect';
928
+
929
+ Cause.Done(); // create a Done<void> signal
930
+ Cause.Done('leftover'); // create Done<string> with a final payload
931
+ Cause.done('leftover'); // Effect<never, Cause.Done<string>>
932
+ Cause.isDone(value); // type guard
933
+ ```
934
+
935
+ A `Fail` reason carrying `Done` can be combined with a real failure, for example when stream completion and resource finalization both settle unsuccessfully. Pull/stream completion handlers remove the `Done` signal but preserve any other merged failure reasons; do not treat the presence of `Done` as permission to discard the whole cause.
936
+
937
+ ## Exhaustive Error Handling with Match
938
+
939
+ Use Match for exhaustive error handling with compile-time guarantees:
940
+
941
+ ```typescript
942
+ import * as Effect from 'effect/Effect';
943
+ import * as Match from 'effect/Match';
944
+ import * as Schema from 'effect/Schema';
945
+
946
+ declare const dangerousOperation: () => Effect.Effect<string, AppError>;
947
+
948
+ class ConnectionError extends Schema.TaggedError<ConnectionError>()(
949
+ 'ConnectionError',
950
+ {
951
+ message: Schema.String
952
+ }
953
+ ) {}
954
+
955
+ class AuthError extends Schema.TaggedError<AuthError>()('AuthError', {
956
+ message: Schema.String
957
+ }) {}
958
+
959
+ class DataError extends Schema.TaggedError<DataError>()('DataError', {
960
+ message: Schema.String
961
+ }) {}
962
+
963
+ type AppError = ConnectionError | AuthError | DataError;
964
+
965
+ const handleError = (error: AppError): Effect.Effect<string> =>
966
+ Match.value(error).pipe(
967
+ Match.tag('ConnectionError', () =>
968
+ Effect.succeed('Please check your network connection')
969
+ ),
970
+ Match.tag('AuthError', () => Effect.succeed('Authentication required')),
971
+ Match.tag('DataError', (err) =>
972
+ Effect.succeed(`Data error: ${err.message}`)
973
+ ),
974
+ Match.exhaustive // Compiler ensures all cases handled
975
+ );
976
+
977
+ const program = dangerousOperation().pipe(Effect.catch(handleError));
978
+ ```
979
+
980
+ ## Error Transformation
981
+
982
+ ### mapError - Transform Error Type
983
+
984
+ ```typescript
985
+ import * as Effect from 'effect/Effect';
986
+ import * as Schema from 'effect/Schema';
987
+
988
+ declare const fetchFromDatabase: () => Effect.Effect<Data, InfrastructureError>;
989
+
990
+ interface Data {
991
+ readonly value: string;
992
+ }
993
+
994
+ class DomainError extends Schema.TaggedError<DomainError>()(
995
+ 'DomainError',
996
+ {
997
+ message: Schema.String
998
+ }
999
+ ) {}
1000
+
1001
+ class InfrastructureError extends Schema.TaggedError<InfrastructureError>()(
1002
+ 'InfrastructureError',
1003
+ { message: Schema.String, cause: Schema.optional(Schema.Unknown) }
1004
+ ) {}
1005
+
1006
+ // Transform infrastructure errors to domain errors
1007
+ const program = fetchFromDatabase().pipe(
1008
+ Effect.mapError(
1009
+ (infraError: InfrastructureError) =>
1010
+ new DomainError({
1011
+ message: `Database operation failed: ${infraError.message}`
1012
+ })
1013
+ )
1014
+ );
1015
+ ```
1016
+
1017
+ ### Idempotent Error Wrapping
1018
+
1019
+ When writing reusable error-mapping combinators shared across multiple call sites, guard against double-wrapping:
1020
+
1021
+ ```typescript
1022
+ import { Effect, Schema } from 'effect';
1023
+
1024
+ class ServiceError extends Schema.TaggedError<ServiceError>()(
1025
+ 'ServiceError',
1026
+ {
1027
+ message: Schema.String,
1028
+ cause: Schema.optional(Schema.Unknown)
1029
+ }
1030
+ ) {}
1031
+
1032
+ const mapServiceError =
1033
+ (message = 'Service operation failed') =>
1034
+ <A, E, R>(
1035
+ effect: Effect.Effect<A, E, R>
1036
+ ): Effect.Effect<A, ServiceError, R> =>
1037
+ effect.pipe(
1038
+ Effect.mapError((cause) =>
1039
+ cause instanceof ServiceError
1040
+ ? cause
1041
+ : new ServiceError({ message, cause })
1042
+ )
1043
+ );
1044
+ ```
1045
+
1046
+ The `instanceof` guard prevents double-wrapping when an upstream operation already returns the target error type.
1047
+
1048
+ ## Error Recovery Patterns
1049
+
1050
+ Retry timing and recurrence design belong in the dedicated effect-scheduling skill. This skill determines which failures are typed and recoverable; scheduling determines whether, when, and how often an idempotent operation is retried.
1051
+
1052
+ ### Translate at Service Boundaries
1053
+
1054
+ Translate infrastructure errors into the service's public error vocabulary at the boundary that owns the abstraction, using `mapError`, `catchTag`, or `catchTags`. Preserve useful context in the translated error and avoid repeatedly wrapping an error already in the target vocabulary.
1055
+
1056
+ Typed error combinators preserve defects and interruption. Keep that property: do not use blanket `catchCause`, `ignoreCause`, or cause-to-domain-error conversion in ordinary services, because they can turn cancellation into a recoverable failure. Cause-level recovery belongs only at an explicit supervision/runtime boundary; detect and re-propagate interruption rather than logging it as an operational error and continuing.
1057
+
1058
+ ### Fallback with orElse
1059
+
1060
+ ```typescript
1061
+ import * as Effect from 'effect/Effect';
1062
+ import * as Schema from 'effect/Schema';
1063
+
1064
+ interface Data {
1065
+ readonly value: string;
1066
+ }
1067
+
1068
+ class PrimaryServiceError extends Schema.TaggedError<PrimaryServiceError>()(
1069
+ 'PrimaryServiceError',
1070
+ { message: Schema.String }
1071
+ ) {}
1072
+
1073
+ class SecondaryServiceError extends Schema.TaggedError<SecondaryServiceError>()(
1074
+ 'SecondaryServiceError',
1075
+ { message: Schema.String }
1076
+ ) {}
1077
+
1078
+ const primaryService: Effect.Effect<Data, PrimaryServiceError> = Effect.fail(
1079
+ new PrimaryServiceError({ message: 'Primary down' })
1080
+ );
1081
+ const secondaryService: Effect.Effect<Data, SecondaryServiceError> =
1082
+ Effect.fail(new SecondaryServiceError({ message: 'Secondary down' }));
1083
+
1084
+ // Try primary, fallback to secondary
1085
+ // Effect<Data, SecondaryServiceError, Dependencies>
1086
+ const program = primaryService.pipe(Effect.orElse(() => secondaryService));
1087
+ ```
1088
+
1089
+ ### Retry with Schedule
1090
+
1091
+ For policy selection, bounds, backoff, jitter, rate-limit delays, polling, and idempotency requirements, see the effect-scheduling skill.
1092
+
1093
+ ```typescript
1094
+ import * as Effect from 'effect/Effect';
1095
+ import * as Schedule from 'effect/Schedule';
1096
+ import * as Schema from 'effect/Schema';
1097
+
1098
+ interface Data {
1099
+ readonly value: string;
1100
+ }
1101
+
1102
+ class TransientError extends Schema.TaggedError<TransientError>()(
1103
+ 'TransientError',
1104
+ {
1105
+ message: Schema.String
1106
+ }
1107
+ ) {}
1108
+
1109
+ const unreliableOperation: Effect.Effect<Data, TransientError> = Effect.fail(
1110
+ new TransientError({ message: 'Temporary failure' })
1111
+ );
1112
+
1113
+ // Retry with exponential backoff
1114
+ const program = unreliableOperation.pipe(
1115
+ Effect.retry(
1116
+ Schedule.exponential('100 millis').pipe(
1117
+ Schedule.upTo({ times: 5 })
1118
+ ) // Max 5 retries
1119
+ )
1120
+ );
1121
+ ```
1122
+
1123
+ ### Provide Default Value
1124
+
1125
+ ```typescript
1126
+ import * as Effect from 'effect/Effect';
1127
+ import * as Schema from 'effect/Schema';
1128
+
1129
+ declare const getDefaultConfig: () => Config;
1130
+
1131
+ interface Config {
1132
+ readonly port: number;
1133
+ readonly host: string;
1134
+ }
1135
+
1136
+ class FetchError extends Schema.TaggedError<FetchError>()('FetchError', {
1137
+ message: Schema.String
1138
+ }) {}
1139
+
1140
+ const fetchConfig: Effect.Effect<Config, FetchError> = Effect.fail(
1141
+ new FetchError({ message: 'Config not available' })
1142
+ );
1143
+
1144
+ // Provide default on failure
1145
+ const program = fetchConfig.pipe(
1146
+ Effect.orElseSucceed(() => getDefaultConfig())
1147
+ );
1148
+ ```
1149
+
1150
+ ### Convert Error to Option
1151
+
1152
+ ```typescript
1153
+ import * as Effect from 'effect/Effect';
1154
+ import * as Schema from 'effect/Schema';
1155
+
1156
+ interface Item {
1157
+ readonly id: string;
1158
+ readonly name: string;
1159
+ }
1160
+
1161
+ class NotFoundError extends Schema.TaggedError<NotFoundError>()(
1162
+ 'NotFoundError',
1163
+ {
1164
+ message: Schema.String
1165
+ }
1166
+ ) {}
1167
+
1168
+ const findItem: Effect.Effect<Item, NotFoundError> = Effect.fail(
1169
+ new NotFoundError({ message: 'Not found' })
1170
+ );
1171
+
1172
+ // Convert to Option (None if error)
1173
+ // Effect<Option<Item>, never, Dependencies>
1174
+ const program = findItem.pipe(Effect.option);
1175
+ ```
1176
+
1177
+ ## Error Channel vs Defect Operators
1178
+
1179
+ ### Converting Errors to Defects
1180
+
1181
+ ```typescript
1182
+ import * as Effect from 'effect/Effect';
1183
+ import * as Schema from 'effect/Schema';
1184
+
1185
+ interface Config {
1186
+ readonly port: number;
1187
+ readonly host: string;
1188
+ }
1189
+
1190
+ class ConfigError extends Schema.TaggedError<ConfigError>()(
1191
+ 'ConfigError',
1192
+ {
1193
+ message: Schema.String
1194
+ }
1195
+ ) {}
1196
+
1197
+ const loadConfig: Effect.Effect<Config, ConfigError> = Effect.fail(
1198
+ new ConfigError({ message: 'Missing config' })
1199
+ );
1200
+
1201
+ // Convert error to defect (terminates fiber)
1202
+ const program = loadConfig.pipe(
1203
+ Effect.orDie // Error becomes a defect
1204
+ );
1205
+
1206
+ // With custom defect message
1207
+ const program2 = loadConfig.pipe(
1208
+ Effect.orDieWith(
1209
+ (error) =>
1210
+ new Error(`Fatal: Configuration failed to load: ${error._tag}`)
1211
+ )
1212
+ );
1213
+ ```
1214
+
1215
+ ### Handling Defects (Boundary Only)
1216
+
1217
+ ```typescript
1218
+ import * as Effect from 'effect/Effect';
1219
+
1220
+ declare const dangerousPlugin: () => Effect.Effect<unknown>;
1221
+ declare const getDefaultPluginBehavior: () => unknown;
1222
+
1223
+ // NOTE: ONLY use at application boundaries
1224
+ const safeProgram = dangerousPlugin().pipe(
1225
+ Effect.catchDefect((defect) =>
1226
+ Effect.logError(`Plugin crashed: ${defect}`).pipe(
1227
+ Effect.as(getDefaultPluginBehavior())
1228
+ )
1229
+ )
1230
+ );
1231
+ ```
1232
+
1233
+ ## ErrorReporter (v4)
1234
+
1235
+ The `ErrorReporter` module is new in v4. It provides pluggable, structured error reporting with severity levels and metadata.
1236
+
1237
+ ### Defining a Reporter
1238
+
1239
+ ```typescript
1240
+ import { ErrorReporter } from 'effect';
1241
+
1242
+ // Create a custom reporter — the callback receives a single options object
1243
+ const myReporter = ErrorReporter.make(({ cause, error, severity, attributes }) => {
1244
+ console.error(`[${severity}]`, error.message, attributes);
1245
+ });
1246
+
1247
+ // Register reporters via Layer
1248
+ const ReporterLayer = ErrorReporter.layer([myReporter]);
1249
+ ```
1250
+
1251
+ ### Reporting Errors
1252
+
1253
+ ```typescript
1254
+ import { Effect, ErrorReporter } from 'effect';
1255
+
1256
+ // Automatically report errors from an effect
1257
+ const program = riskyOperation.pipe(Effect.withErrorReporting);
1258
+
1259
+ // Or, to report defects only:
1260
+ const defectsOnly = riskyOperation.pipe(
1261
+ Effect.withErrorReporting({ defectsOnly: true })
1262
+ );
1263
+ ```
1264
+
1265
+ ### Per-Error Annotations
1266
+
1267
+ Error objects can carry reporting annotations as string-keyed properties (the keys are namespaced strings such as `"~effect/ErrorReporter/severity"`):
1268
+
1269
+ ```typescript
1270
+ import { ErrorReporter, Schema } from 'effect';
1271
+
1272
+ class MyError extends Schema.TaggedError<MyError>()('MyError', {
1273
+ message: Schema.String
1274
+ }) {}
1275
+
1276
+ const error = new MyError({ message: 'something went wrong' });
1277
+
1278
+ // Mark an error to be ignored by reporters
1279
+ ErrorReporter.ignore; // string key — set to true to skip reporting
1280
+
1281
+ // Override severity (defaults to "Info" when unset or invalid)
1282
+ ErrorReporter.severity; // string key — "Trace" | "Debug" | "Info" | "Warn" | "Error" | "Fatal"
1283
+
1284
+ // Attach extra structured metadata
1285
+ ErrorReporter.attributes; // string key — Record<string, unknown>
1286
+
1287
+ // Guards
1288
+ ErrorReporter.isIgnored(error); // check if ignored
1289
+ ErrorReporter.getSeverity(error); // read severity
1290
+ ErrorReporter.getAttributes(error); // read attributes
1291
+ ```
1292
+
1293
+ ## Layered Error Handling
1294
+
1295
+ Structure error handling in layers from specific to general:
1296
+
1297
+ ```typescript
1298
+ import * as Effect from 'effect/Effect';
1299
+ import * as Schema from 'effect/Schema';
1300
+
1301
+ declare const validateUserData: (
1302
+ data: UserData
1303
+ ) => Effect.Effect<ValidatedUserData, ValidationError>;
1304
+ declare const saveToDatabase: (
1305
+ data: ValidatedUserData
1306
+ ) => Effect.Effect<string, DatabaseError>;
1307
+ declare const notifyUserCreated: (
1308
+ userId: string
1309
+ ) => Effect.Effect<void, NetworkError>;
1310
+
1311
+ interface UserData {
1312
+ readonly name: string;
1313
+ readonly email: string;
1314
+ }
1315
+
1316
+ interface ValidatedUserData {
1317
+ readonly name: string;
1318
+ readonly email: string;
1319
+ }
1320
+
1321
+ class ValidationError extends Schema.TaggedError<ValidationError>()(
1322
+ 'ValidationError',
1323
+ {
1324
+ message: Schema.String
1325
+ }
1326
+ ) {}
1327
+
1328
+ class DatabaseError extends Schema.TaggedError<DatabaseError>()(
1329
+ 'DatabaseError',
1330
+ {
1331
+ message: Schema.String
1332
+ }
1333
+ ) {}
1334
+
1335
+ class NetworkError extends Schema.TaggedError<NetworkError>()(
1336
+ 'NetworkError',
1337
+ {
1338
+ message: Schema.String
1339
+ }
1340
+ ) {}
1341
+
1342
+ class UnknownError extends Schema.TaggedError<UnknownError>()(
1343
+ 'UnknownError',
1344
+ {
1345
+ message: Schema.String,
1346
+ cause: Schema.optional(Schema.Unknown)
1347
+ }
1348
+ ) {}
1349
+
1350
+ const createUser = (data: UserData) =>
1351
+ Effect.gen(function* () {
1352
+ // Layer 1: Validate input
1353
+ const validated = yield* validateUserData(data).pipe(
1354
+ Effect.catchTag('ValidationError', (error) =>
1355
+ Effect.fail(
1356
+ new UnknownError({ message: error.message, cause: error })
1357
+ )
1358
+ )
1359
+ );
1360
+
1361
+ // Layer 2: Database operation
1362
+ const userId = yield* saveToDatabase(validated).pipe(
1363
+ Effect.catchTag('DatabaseError', (error) =>
1364
+ Effect.fail(
1365
+ new UnknownError({ message: error.message, cause: error })
1366
+ )
1367
+ )
1368
+ );
1369
+
1370
+ // Layer 3: Network notification
1371
+ yield* notifyUserCreated(userId).pipe(
1372
+ Effect.catchTag('NetworkError', (error) =>
1373
+ // Non-critical: log but don't fail
1374
+ Effect.logWarning(`Failed to notify: ${error._tag}`)
1375
+ )
1376
+ );
1377
+
1378
+ return userId;
1379
+ });
1380
+ ```
1381
+
1382
+ ## Domain-Specific Error Patterns
1383
+
1384
+ ### HTTP Response Discrimination
1385
+
1386
+ Model ambiguous HTTP responses (where the body structure differs for success vs error) as a `Schema.Union` of `Schema.Class` types:
1387
+
1388
+ ```typescript
1389
+ import { Effect, Schema } from 'effect';
1390
+ import { HttpClientResponse } from 'effect/unstable/HttpClient';
1391
+
1392
+ class TokenSuccess extends Schema.Class<TokenSuccess>('TokenSuccess')({
1393
+ access_token: AccessToken,
1394
+ expires_in: Schema.Number
1395
+ }) {}
1396
+
1397
+ class TokenError extends Schema.Class<TokenError>('TokenError')({
1398
+ error: Schema.String,
1399
+ error_description: Schema.optional(Schema.String)
1400
+ }) {}
1401
+
1402
+ const TokenResponse = Schema.Union([TokenSuccess, TokenError]);
1403
+
1404
+ // Decode and discriminate
1405
+ const response = yield* HttpClientResponse.schemaBodyJson(TokenResponse)(res);
1406
+ if (response instanceof TokenError) {
1407
+ return (
1408
+ yield*
1409
+ new AuthError({
1410
+ message: response.error_description ?? response.error
1411
+ })
1412
+ );
1413
+ }
1414
+ ```
1415
+
1416
+ This replaces ad-hoc optional-field checking (`if (!body.access_token)`) with compile-time-safe discrimination via `instanceof`.
1417
+
1418
+ ### Repository Errors
1419
+
1420
+ ```typescript
1421
+ import * as Schema from 'effect/Schema';
1422
+
1423
+ export class EntityNotFound extends Schema.TaggedError<EntityNotFound>()(
1424
+ 'EntityNotFound',
1425
+ {
1426
+ entityType: Schema.String,
1427
+ id: Schema.String,
1428
+ message: Schema.String
1429
+ },
1430
+ { httpApiStatus: 404, description: 'Requested entity does not exist.' }
1431
+ ) {}
1432
+
1433
+ export class DuplicateEntity extends Schema.TaggedError<DuplicateEntity>()(
1434
+ 'DuplicateEntity',
1435
+ {
1436
+ entityType: Schema.String,
1437
+ id: Schema.String,
1438
+ message: Schema.String
1439
+ },
1440
+ { httpApiStatus: 409, description: 'Entity already exists.' }
1441
+ ) {}
1442
+
1443
+ export class QueryError extends Schema.TaggedError<QueryError>()(
1444
+ 'QueryError',
1445
+ {
1446
+ query: Schema.String,
1447
+ message: Schema.String,
1448
+ cause: Schema.optional(Schema.Unknown)
1449
+ },
1450
+ { description: 'Database query failed.' }
1451
+ ) {}
1452
+
1453
+ export type RepositoryError = EntityNotFound | DuplicateEntity | QueryError;
1454
+ ```
1455
+
1456
+ ### Service Errors
1457
+
1458
+ ```typescript
1459
+ import * as Schema from 'effect/Schema';
1460
+
1461
+ export class ServiceUnavailable extends Schema.TaggedError<ServiceUnavailable>()(
1462
+ 'ServiceUnavailable',
1463
+ {
1464
+ service: Schema.String,
1465
+ message: Schema.String,
1466
+ retryAfter: Schema.optional(Schema.Number)
1467
+ },
1468
+ { httpApiStatus: 503, description: 'Upstream service is unavailable.' }
1469
+ ) {}
1470
+
1471
+ export class ServiceTimeout extends Schema.TaggedError<ServiceTimeout>()(
1472
+ 'ServiceTimeout',
1473
+ {
1474
+ service: Schema.String,
1475
+ message: Schema.String,
1476
+ timeoutMs: Schema.Number
1477
+ },
1478
+ { httpApiStatus: 504, description: 'Upstream service timed out.' }
1479
+ ) {}
1480
+
1481
+ export class InvalidResponse extends Schema.TaggedError<InvalidResponse>()(
1482
+ 'InvalidResponse',
1483
+ {
1484
+ service: Schema.String,
1485
+ message: Schema.String,
1486
+ response: Schema.optional(Schema.Unknown)
1487
+ },
1488
+ { description: 'Upstream service returned an unexpected response.' }
1489
+ ) {}
1490
+
1491
+ export type ServiceError =
1492
+ | ServiceUnavailable
1493
+ | ServiceTimeout
1494
+ | InvalidResponse;
1495
+ ```
1496
+
1497
+ ### Error Boundaries
1498
+
1499
+ ```typescript
1500
+ import * as Effect from 'effect/Effect';
1501
+ import * as Schema from 'effect/Schema';
1502
+
1503
+ declare const processRequest: (
1504
+ request: Request
1505
+ ) => Effect.Effect<Response, ValidationError | NotFoundError | DatabaseError>;
1506
+ declare const HttpResponse: {
1507
+ badRequest: (message: string) => Response;
1508
+ notFound: () => Response;
1509
+ internalServerError: () => Response;
1510
+ };
1511
+
1512
+ interface Request {
1513
+ readonly url: string;
1514
+ }
1515
+
1516
+ interface Response {
1517
+ readonly status: number;
1518
+ }
1519
+
1520
+ class ValidationError extends Schema.TaggedError<ValidationError>()(
1521
+ 'ValidationError',
1522
+ {
1523
+ message: Schema.String
1524
+ }
1525
+ ) {}
1526
+
1527
+ class NotFoundError extends Schema.TaggedError<NotFoundError>()(
1528
+ 'NotFoundError',
1529
+ {
1530
+ message: Schema.String
1531
+ }
1532
+ ) {}
1533
+
1534
+ class DatabaseError extends Schema.TaggedError<DatabaseError>()(
1535
+ 'DatabaseError',
1536
+ {
1537
+ message: Schema.String
1538
+ }
1539
+ ) {}
1540
+
1541
+ // Define clear boundaries where errors are handled
1542
+ const apiEndpoint = (request: Request) =>
1543
+ Effect.gen(function* () {
1544
+ const result = yield* processRequest(request);
1545
+ return result;
1546
+ }).pipe(
1547
+ // Error boundary: convert all errors to HTTP responses
1548
+ Effect.catchTags({
1549
+ ValidationError: (error) =>
1550
+ Effect.succeed(HttpResponse.badRequest(error.message)),
1551
+ NotFoundError: () => Effect.succeed(HttpResponse.notFound()),
1552
+ DatabaseError: (error) =>
1553
+ Effect.logError(error).pipe(
1554
+ Effect.as(HttpResponse.internalServerError())
1555
+ )
1556
+ })
1557
+ );
1558
+ ```
1559
+
1560
+ ## Quality Checklist
1561
+
1562
+ Before completing error handling implementation:
1563
+
1564
+ - [ ] All domain errors use `Schema.TaggedError` with a `message` field
1565
+ - [ ] Error types have meaningful, specific names and `description` annotation
1566
+ - [ ] Errors include relevant context (ids, values, reasons)
1567
+ - [ ] Business failures in error channel, programmer errors as defects
1568
+ - [ ] catchTag/catchTags used for specific error handling
1569
+ - [ ] catch (NOT catchAll) only when handling truly all error types
1570
+ - [ ] Error transformations preserve important context
1571
+ - [ ] Recovery strategies match business requirements
1572
+ - [ ] Defect handling only at application boundaries
1573
+ - [ ] Error types exported from domain modules
1574
+ - [ ] Tests cover error scenarios
1575
+ - [ ] Type signatures accurately reflect error channel
1576
+ - [ ] No v3 API names used (catchAll, catchSome, \*Exception, etc.)
1577
+ - [ ] Errors with `reason` union consider `catchReason`/`catchReasons`/`unwrapReason`
1578
+ - [ ] HTTP-facing errors carry `httpApiStatus` annotation
1579
+ - [ ] Error wrapping uses appropriate `cause` field schema (`Schema.Defect()`/`Schema.Unknown`/`Schema.String`)
1580
+
1581
+ Your error handling implementations should be type-safe, exhaustive, and maintain clear separation between expected failures and programmer errors. Always use v4 API names.