mdcontext 0.0.1 → 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.
- package/.changeset/README.md +28 -0
- package/.changeset/config.json +11 -0
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/.github/workflows/release.yml +113 -0
- package/.tldrignore +112 -0
- package/BACKLOG.md +338 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +434 -11
- package/biome.json +36 -0
- package/cspell.config.yaml +14 -0
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +88 -0
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +803 -0
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1629 -0
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +5458 -0
- package/dist/index.d.ts +653 -0
- package/dist/index.js +79 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +472 -0
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +625 -0
- package/docs/020-current-implementation.md +364 -0
- package/docs/021-DOGFOODING-FINDINGS.md +175 -0
- package/docs/BACKLOG.md +80 -0
- package/docs/CONFIG.md +1123 -0
- package/docs/DESIGN.md +439 -0
- package/docs/ERRORS.md +383 -0
- package/docs/PROJECT.md +88 -0
- package/docs/ROADMAP.md +407 -0
- package/docs/summarization.md +320 -0
- package/docs/test-links.md +9 -0
- package/justfile +40 -0
- package/package.json +74 -9
- package/pnpm-workspace.yaml +5 -0
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -0
- package/research/config-analysis/01-current-implementation.md +470 -0
- package/research/config-analysis/02-strategy-recommendation.md +428 -0
- package/research/config-analysis/03-task-candidates.md +715 -0
- package/research/config-analysis/033-research-configuration-management.md +828 -0
- package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
- package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
- package/research/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -0
- package/research/dogfood/consolidated-tool-evaluation.md +373 -0
- package/research/dogfood/strategy-a/a-synthesis.md +184 -0
- package/research/dogfood/strategy-a/a1-docs.md +226 -0
- package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
- package/research/dogfood/strategy-a/a3-llm.md +164 -0
- package/research/dogfood/strategy-b/b-synthesis.md +228 -0
- package/research/dogfood/strategy-b/b1-architecture.md +207 -0
- package/research/dogfood/strategy-b/b2-gaps.md +258 -0
- package/research/dogfood/strategy-b/b3-workflows.md +250 -0
- package/research/dogfood/strategy-c/c-synthesis.md +451 -0
- package/research/dogfood/strategy-c/c1-explorer.md +192 -0
- package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
- package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
- package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
- package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
- package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
- package/research/effect-cli-error-handling.md +845 -0
- package/research/effect-errors-as-values.md +943 -0
- package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
- package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
- package/research/errors-task-analysis/embeddings-analysis.md +709 -0
- package/research/errors-task-analysis/index-search-analysis.md +812 -0
- package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-error-analysis.md +521 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
- package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
- package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
- package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
- package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
- package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
- package/research/research-quality-review.md +834 -0
- package/research/semantic-search/002-research-embedding-models.md +490 -0
- package/research/semantic-search/003-research-rag-alternatives.md +523 -0
- package/research/semantic-search/004-research-vector-search.md +841 -0
- package/research/semantic-search/032-research-semantic-search.md +427 -0
- package/research/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -0
- package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
- package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
- package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
- package/research/task-management-2026/03-lightweight-file-based.md +567 -0
- package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
- package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
- package/research/task-management-2026/linear/02-api-integrations.md +930 -0
- package/research/task-management-2026/linear/03-ai-features.md +368 -0
- package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
- package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
- package/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +58 -0
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +210 -0
- package/src/cli/argv-preprocessor.ts +202 -0
- package/src/cli/cli.test.ts +627 -0
- package/src/cli/commands/backlinks.ts +54 -0
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +285 -0
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +480 -0
- package/src/cli/commands/index.ts +16 -0
- package/src/cli/commands/links.ts +52 -0
- package/src/cli/commands/search.ts +1281 -0
- package/src/cli/commands/stats.ts +149 -0
- package/src/cli/commands/tree.ts +128 -0
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +341 -0
- package/src/cli/help.ts +588 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/main.ts +435 -0
- package/src/cli/options.ts +41 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/typo-suggester.test.ts +105 -0
- package/src/cli/typo-suggester.ts +130 -0
- package/src/cli/utils.ts +259 -0
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/index.ts +1 -0
- package/src/core/types.ts +113 -0
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +10 -0
- package/src/embeddings/openai-provider.ts +414 -0
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +1270 -0
- package/src/embeddings/types.ts +359 -0
- package/src/embeddings/vector-store.ts +708 -0
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/index.ts +4 -0
- package/src/index/indexer.ts +684 -0
- package/src/index/storage.ts +260 -0
- package/src/index/types.ts +147 -0
- package/src/index/watcher.ts +189 -0
- package/src/index.ts +30 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +612 -0
- package/src/parser/index.ts +1 -0
- package/src/parser/parser.test.ts +291 -0
- package/src/parser/parser.ts +394 -0
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +392 -0
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/query-parser.test.ts +260 -0
- package/src/search/query-parser.ts +319 -0
- package/src/search/searcher.test.ts +280 -0
- package/src/search/searcher.ts +724 -0
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -0
- package/src/summarize/budget-bugs.test.ts +620 -0
- package/src/summarize/formatters.ts +419 -0
- package/src/summarize/index.ts +20 -0
- package/src/summarize/summarizer.test.ts +275 -0
- package/src/summarize/summarizer.ts +597 -0
- package/src/summarize/verify-bugs.test.ts +238 -0
- package/src/types/huggingface-transformers.d.ts +66 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/tokens.test.ts +142 -0
- package/src/utils/tokens.ts +186 -0
- package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/config.json +8 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
- package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -0
- package/tests/fixtures/cli/README.md +9 -0
- package/tests/fixtures/cli/api-reference.md +11 -0
- package/tests/fixtures/cli/getting-started.md +11 -0
- package/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +16 -0
- package/vitest.setup.ts +12 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
# Effect: Errors as Values Pattern
|
|
2
|
+
|
|
3
|
+
> A comprehensive guide to Effect's type-safe error handling system
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Core Concept: Errors as Values](#core-concept-errors-as-values)
|
|
8
|
+
2. [Error Types in Effect](#error-types-in-effect)
|
|
9
|
+
3. [Error Handling Patterns](#error-handling-patterns)
|
|
10
|
+
4. [Best Practices for CLI Applications](#best-practices-for-cli-applications)
|
|
11
|
+
5. [Anti-patterns to Avoid](#anti-patterns-to-avoid)
|
|
12
|
+
6. [Sources](#sources)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Concept: Errors as Values
|
|
17
|
+
|
|
18
|
+
### The Problem with Traditional Error Handling
|
|
19
|
+
|
|
20
|
+
In traditional TypeScript, error handling relies on try/catch and thrown exceptions:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// Traditional TypeScript - no type safety for errors
|
|
24
|
+
function divide(a: number, b: number): number {
|
|
25
|
+
if (b === 0) {
|
|
26
|
+
throw new Error("Division by zero");
|
|
27
|
+
}
|
|
28
|
+
return a / b;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The type signature gives NO indication this can fail
|
|
32
|
+
// TypeScript infers: (a: number, b: number) => number
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Problems with this approach:**
|
|
36
|
+
|
|
37
|
+
- Type signatures don't indicate that functions can throw
|
|
38
|
+
- Impossible to know what errors a function might produce
|
|
39
|
+
- Easy to forget error handling as codebases grow
|
|
40
|
+
- No compile-time enforcement of error handling
|
|
41
|
+
|
|
42
|
+
### Effect's Solution: Track Errors in the Type System
|
|
43
|
+
|
|
44
|
+
Effect treats errors as **first-class values** tracked in the type system through the `Effect<A, E, R>` type:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
Effect<A, E, R>;
|
|
48
|
+
// ^ ^ ^
|
|
49
|
+
// | | └── R: Requirements (dependencies/context)
|
|
50
|
+
// | └───── E: Error type (what can go wrong)
|
|
51
|
+
// └──────── A: Success type (the result)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Key insight**: By making errors explicit in the type signature, you always know:
|
|
55
|
+
|
|
56
|
+
- What a function returns on success
|
|
57
|
+
- What errors it might produce
|
|
58
|
+
- What dependencies it requires
|
|
59
|
+
|
|
60
|
+
### Effect vs Exceptions: A Comparison
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Traditional: errors are invisible in types
|
|
64
|
+
function parseJSON(str: string): unknown {
|
|
65
|
+
return JSON.parse(str); // Can throw SyntaxError!
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Effect: errors are visible and tracked
|
|
69
|
+
import { Effect } from "effect";
|
|
70
|
+
|
|
71
|
+
class ParseError extends Data.TaggedError("ParseError")<{
|
|
72
|
+
message: string;
|
|
73
|
+
}> {}
|
|
74
|
+
|
|
75
|
+
const parseJSON = (str: string): Effect.Effect<unknown, ParseError> =>
|
|
76
|
+
Effect.try({
|
|
77
|
+
try: () => JSON.parse(str),
|
|
78
|
+
catch: (e) => new ParseError({ message: String(e) }),
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Automatic Error Union Tracking
|
|
83
|
+
|
|
84
|
+
When you compose effects, Effect automatically tracks all possible errors as a union:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { Effect, Data } from "effect"
|
|
88
|
+
|
|
89
|
+
class NetworkError extends Data.TaggedError("NetworkError")<{}> {}
|
|
90
|
+
class ValidationError extends Data.TaggedError("ValidationError")<{}> {}
|
|
91
|
+
|
|
92
|
+
const fetchData: Effect.Effect<Data, NetworkError> = // ...
|
|
93
|
+
const validateData: Effect.Effect<ValidData, ValidationError> = // ...
|
|
94
|
+
|
|
95
|
+
// Composed effect automatically has union error type
|
|
96
|
+
const program: Effect.Effect<ValidData, NetworkError | ValidationError> =
|
|
97
|
+
fetchData.pipe(
|
|
98
|
+
Effect.flatMap(data => validateData)
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Why This Matters
|
|
103
|
+
|
|
104
|
+
1. **Compile-time safety**: The compiler forces you to handle all error cases
|
|
105
|
+
2. **Exhaustive handling**: You can pattern match on error types
|
|
106
|
+
3. **Self-documenting code**: Type signatures tell the full story
|
|
107
|
+
4. **Refactoring confidence**: Change an error type and the compiler shows all affected code
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Error Types in Effect
|
|
112
|
+
|
|
113
|
+
### Tagged Errors with Data.TaggedError
|
|
114
|
+
|
|
115
|
+
The recommended way to define errors in Effect is using `Data.TaggedError`:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { Data } from "effect";
|
|
119
|
+
|
|
120
|
+
// Define custom errors with unique tags
|
|
121
|
+
class NetworkError extends Data.TaggedError("NetworkError")<{
|
|
122
|
+
url: string;
|
|
123
|
+
statusCode: number;
|
|
124
|
+
}> {}
|
|
125
|
+
|
|
126
|
+
class ValidationError extends Data.TaggedError("ValidationError")<{
|
|
127
|
+
field: string;
|
|
128
|
+
message: string;
|
|
129
|
+
}> {}
|
|
130
|
+
|
|
131
|
+
class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{
|
|
132
|
+
path: string;
|
|
133
|
+
}> {}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Key benefits of TaggedError:**
|
|
137
|
+
|
|
138
|
+
- Automatically adds a `_tag` discriminant field
|
|
139
|
+
- Includes `cause` and `stack` properties
|
|
140
|
+
- Can be used both as a value (`new NetworkError(...)`) and a type
|
|
141
|
+
- Enables precise error matching with `catchTag`
|
|
142
|
+
|
|
143
|
+
### Error Hierarchies and Unions
|
|
144
|
+
|
|
145
|
+
Effect tracks error unions automatically, enabling hierarchical error handling:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// Domain-specific errors
|
|
149
|
+
class AuthError extends Data.TaggedError("AuthError")<{
|
|
150
|
+
reason: "invalid_token" | "expired" | "forbidden";
|
|
151
|
+
}> {}
|
|
152
|
+
|
|
153
|
+
class ApiError extends Data.TaggedError("ApiError")<{
|
|
154
|
+
endpoint: string;
|
|
155
|
+
statusCode: number;
|
|
156
|
+
}> {}
|
|
157
|
+
|
|
158
|
+
// Application-level error union
|
|
159
|
+
type AppError = AuthError | ApiError | ValidationError;
|
|
160
|
+
|
|
161
|
+
// Effect tracks these automatically
|
|
162
|
+
const fetchUser = (id: string): Effect.Effect<User, AuthError | ApiError> =>
|
|
163
|
+
// ...
|
|
164
|
+
|
|
165
|
+
const validateUser = (user: User): Effect.Effect<ValidUser, ValidationError> =>
|
|
166
|
+
// ...
|
|
167
|
+
|
|
168
|
+
// Composed: Effect<ValidUser, AuthError | ApiError | ValidationError>
|
|
169
|
+
const program = fetchUser("123").pipe(
|
|
170
|
+
Effect.flatMap(validateUser)
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### The Three Types of Failures
|
|
175
|
+
|
|
176
|
+
Effect distinguishes between three fundamentally different failure modes:
|
|
177
|
+
|
|
178
|
+
#### 1. Expected Errors (Fail)
|
|
179
|
+
|
|
180
|
+
Errors that are part of your domain logic and should be handled:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { Effect, Data } from "effect";
|
|
184
|
+
|
|
185
|
+
class InsufficientFundsError extends Data.TaggedError("InsufficientFunds")<{
|
|
186
|
+
balance: number;
|
|
187
|
+
required: number;
|
|
188
|
+
}> {}
|
|
189
|
+
|
|
190
|
+
const withdraw = (
|
|
191
|
+
amount: number,
|
|
192
|
+
): Effect.Effect<number, InsufficientFundsError> =>
|
|
193
|
+
getBalance().pipe(
|
|
194
|
+
Effect.flatMap((balance) =>
|
|
195
|
+
balance < amount
|
|
196
|
+
? Effect.fail(new InsufficientFundsError({ balance, required: amount }))
|
|
197
|
+
: Effect.succeed(balance - amount),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- Tracked in the `E` type parameter
|
|
203
|
+
- Should be handled with `catchTag`, `catchAll`, etc.
|
|
204
|
+
- Represent recoverable business logic failures
|
|
205
|
+
|
|
206
|
+
#### 2. Defects (Die)
|
|
207
|
+
|
|
208
|
+
Unexpected errors that indicate bugs or unrecoverable situations:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { Effect } from "effect";
|
|
212
|
+
|
|
213
|
+
// Create a defect - unrecoverable error
|
|
214
|
+
const divide = (a: number, b: number): Effect.Effect<number> =>
|
|
215
|
+
b === 0
|
|
216
|
+
? Effect.die(new Error("Division by zero - this should never happen"))
|
|
217
|
+
: Effect.succeed(a / b);
|
|
218
|
+
|
|
219
|
+
// Defects don't appear in the error type parameter!
|
|
220
|
+
// The effect type is Effect<number, never>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- NOT tracked in the `E` type parameter (type is `never`)
|
|
224
|
+
- Indicate programming errors or impossible states
|
|
225
|
+
- Should NOT be recovered from in normal operation
|
|
226
|
+
- Can be caught with `catchAllDefect` for edge cases (e.g., plugin systems)
|
|
227
|
+
|
|
228
|
+
#### 3. Interruptions (Interrupt)
|
|
229
|
+
|
|
230
|
+
Represent fiber cancellation:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { Effect, Fiber } from "effect";
|
|
234
|
+
|
|
235
|
+
// A long-running operation
|
|
236
|
+
const longTask = Effect.delay(Effect.succeed("done"), 10000);
|
|
237
|
+
|
|
238
|
+
// Interruption occurs when the result is no longer needed
|
|
239
|
+
const program = Effect.gen(function* () {
|
|
240
|
+
const fiber = yield* Effect.fork(longTask);
|
|
241
|
+
yield* Effect.sleep(100);
|
|
242
|
+
yield* Fiber.interrupt(fiber); // Interrupt the fiber
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- Occur when an Effect is cancelled
|
|
247
|
+
- Used for resource cleanup and timeout handling
|
|
248
|
+
- Can be handled with `Effect.onInterrupt`
|
|
249
|
+
|
|
250
|
+
### The Cause Type: Lossless Error Information
|
|
251
|
+
|
|
252
|
+
Effect uses `Cause<E>` to preserve complete failure information:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { Effect, Cause } from "effect";
|
|
256
|
+
|
|
257
|
+
const program = Effect.gen(function* () {
|
|
258
|
+
const cause = yield* Effect.cause(
|
|
259
|
+
Effect.all([
|
|
260
|
+
Effect.fail("error 1"),
|
|
261
|
+
Effect.die("defect"),
|
|
262
|
+
Effect.fail("error 2"),
|
|
263
|
+
]),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Extract just failures
|
|
267
|
+
console.log(Cause.failures(cause));
|
|
268
|
+
// { _id: 'Chunk', values: [ 'error 1', 'error 2' ] }
|
|
269
|
+
|
|
270
|
+
// Extract just defects
|
|
271
|
+
console.log(Cause.defects(cause));
|
|
272
|
+
// { _id: 'Chunk', values: [ 'defect' ] }
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Cause types include:
|
|
277
|
+
|
|
278
|
+
- `Cause.fail(E)` - Expected error
|
|
279
|
+
- `Cause.die(unknown)` - Defect/unexpected error
|
|
280
|
+
- `Cause.interrupt(FiberId)` - Fiber interruption
|
|
281
|
+
- `Cause.sequential(cause1, cause2)` - Sequential failure composition
|
|
282
|
+
- `Cause.parallel(cause1, cause2)` - Parallel failure composition
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Error Handling Patterns
|
|
287
|
+
|
|
288
|
+
### Pattern 1: catchTag - Handle Specific Tagged Errors
|
|
289
|
+
|
|
290
|
+
Use `catchTag` to handle one specific error type:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { Effect, Data } from "effect"
|
|
294
|
+
|
|
295
|
+
class NetworkError extends Data.TaggedError("NetworkError")<{
|
|
296
|
+
message: string;
|
|
297
|
+
}> {}
|
|
298
|
+
|
|
299
|
+
class ValidationError extends Data.TaggedError("ValidationError")<{
|
|
300
|
+
field: string;
|
|
301
|
+
}> {}
|
|
302
|
+
|
|
303
|
+
const program: Effect.Effect<string, NetworkError | ValidationError> = // ...
|
|
304
|
+
|
|
305
|
+
// Handle only NetworkError, ValidationError still propagates
|
|
306
|
+
const handled = program.pipe(
|
|
307
|
+
Effect.catchTag("NetworkError", (error) =>
|
|
308
|
+
Effect.succeed(`Network failed: ${error.message}`)
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
// Type: Effect<string, ValidationError>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Pattern 2: catchTags - Exhaustive Pattern Matching
|
|
315
|
+
|
|
316
|
+
Handle all tagged errors in a single block:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
const fullyHandled = program.pipe(
|
|
320
|
+
Effect.catchTags({
|
|
321
|
+
NetworkError: (error) => Effect.succeed(`Network failed: ${error.message}`),
|
|
322
|
+
ValidationError: (error) =>
|
|
323
|
+
Effect.succeed(`Validation failed on ${error.field}`),
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
// Type: Effect<string, never>
|
|
327
|
+
// Error channel is 'never' - all errors handled!
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Pattern 3: catchAll - Handle All Errors
|
|
331
|
+
|
|
332
|
+
Catch any error, regardless of type:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const recovered = program.pipe(
|
|
336
|
+
Effect.catchAll((error) => Effect.succeed("Fallback value")),
|
|
337
|
+
);
|
|
338
|
+
// Type: Effect<string, never>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Pattern 4: catchSome - Conditional Error Handling
|
|
342
|
+
|
|
343
|
+
Handle errors that match a predicate:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
const partiallyHandled = program.pipe(
|
|
347
|
+
Effect.catchSome((error) => {
|
|
348
|
+
if (error._tag === "NetworkError" && error.statusCode === 404) {
|
|
349
|
+
return Option.some(Effect.succeed("Not found, using default"));
|
|
350
|
+
}
|
|
351
|
+
return Option.none(); // Let other errors propagate
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Pattern 5: catchIf - Predicate-Based Handling
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const handled = program.pipe(
|
|
360
|
+
Effect.catchIf(
|
|
361
|
+
(error): error is NetworkError => error._tag === "NetworkError",
|
|
362
|
+
(networkError) => Effect.succeed(`Handled: ${networkError.message}`),
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Pattern 6: mapError - Transform Error Types
|
|
368
|
+
|
|
369
|
+
Transform errors without handling them:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
class AppError extends Data.TaggedError("AppError")<{
|
|
373
|
+
originalError: unknown;
|
|
374
|
+
context: string;
|
|
375
|
+
}> {}
|
|
376
|
+
|
|
377
|
+
const mapped = program.pipe(
|
|
378
|
+
Effect.mapError(
|
|
379
|
+
(error) =>
|
|
380
|
+
new AppError({
|
|
381
|
+
originalError: error,
|
|
382
|
+
context: "User fetch operation",
|
|
383
|
+
}),
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
// Type: Effect<string, AppError>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Pattern 7: orElse - Provide Fallback Effects
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
const primary: Effect.Effect<User, NetworkError> = fetchUser("123");
|
|
393
|
+
const fallback: Effect.Effect<User, CacheError> = getCachedUser("123");
|
|
394
|
+
|
|
395
|
+
const withFallback = primary.pipe(Effect.orElse(() => fallback));
|
|
396
|
+
// Type: Effect<User, CacheError>
|
|
397
|
+
// Primary's error is replaced if it fails
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Pattern 8: orElseFail - Replace with Different Error
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
class UserNotAvailableError extends Data.TaggedError("UserNotAvailable")<{}> {}
|
|
404
|
+
|
|
405
|
+
const standardized = program.pipe(
|
|
406
|
+
Effect.orElseFail(() => new UserNotAvailableError()),
|
|
407
|
+
);
|
|
408
|
+
// All original errors become UserNotAvailableError
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Pattern 9: orElseSucceed - Provide Default Value
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
const withDefault = fetchUser("123").pipe(
|
|
415
|
+
Effect.orElseSucceed(() => ({ name: "Anonymous", id: "0" })),
|
|
416
|
+
);
|
|
417
|
+
// Type: Effect<User, never>
|
|
418
|
+
// Error channel becomes 'never' - always succeeds
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Pattern 10: retry - Automatic Retries
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { Effect, Schedule } from "effect"
|
|
425
|
+
|
|
426
|
+
const task: Effect.Effect<string, NetworkError> = // ...
|
|
427
|
+
|
|
428
|
+
// Retry up to 3 times
|
|
429
|
+
const retried = task.pipe(
|
|
430
|
+
Effect.retry({ times: 3 })
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Retry with exponential backoff
|
|
434
|
+
const withBackoff = task.pipe(
|
|
435
|
+
Effect.retry(Schedule.exponential("100 millis"))
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Retry only for specific errors
|
|
439
|
+
const selectiveRetry = task.pipe(
|
|
440
|
+
Effect.retry({
|
|
441
|
+
times: 3,
|
|
442
|
+
while: (error) => error._tag === "NetworkError"
|
|
443
|
+
})
|
|
444
|
+
);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Pattern 11: retryOrElse - Retry with Fallback
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const robustFetch = task.pipe(
|
|
451
|
+
Effect.retryOrElse({ times: 3 }, (error, retryCount) =>
|
|
452
|
+
Effect.succeed(`Failed after ${retryCount} retries: ${error.message}`),
|
|
453
|
+
),
|
|
454
|
+
);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Pattern 12: matchCause - Handle All Failure Types
|
|
458
|
+
|
|
459
|
+
Distinguish between failures, defects, and interruptions:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
const program = Effect.matchCause(task, {
|
|
463
|
+
onFailure: (cause) => {
|
|
464
|
+
switch (cause._tag) {
|
|
465
|
+
case "Fail":
|
|
466
|
+
return `Expected error: ${cause.error.message}`;
|
|
467
|
+
case "Die":
|
|
468
|
+
return `Defect: ${cause.defect}`;
|
|
469
|
+
case "Interrupt":
|
|
470
|
+
return `Interrupted by fiber: ${cause.fiberId}`;
|
|
471
|
+
default:
|
|
472
|
+
return "Other failure";
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
onSuccess: (value) => `Success: ${value}`,
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Best Practices for CLI Applications
|
|
482
|
+
|
|
483
|
+
### 1. Define Domain-Specific Error Types
|
|
484
|
+
|
|
485
|
+
Create clear, tagged errors for your CLI's domain:
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { Data } from "effect";
|
|
489
|
+
|
|
490
|
+
// Input/parsing errors
|
|
491
|
+
class InvalidArgumentError extends Data.TaggedError("InvalidArgument")<{
|
|
492
|
+
argument: string;
|
|
493
|
+
expected: string;
|
|
494
|
+
received: string;
|
|
495
|
+
}> {}
|
|
496
|
+
|
|
497
|
+
class MissingRequiredOptionError extends Data.TaggedError(
|
|
498
|
+
"MissingRequiredOption",
|
|
499
|
+
)<{
|
|
500
|
+
option: string;
|
|
501
|
+
}> {}
|
|
502
|
+
|
|
503
|
+
// File system errors
|
|
504
|
+
class FileNotFoundError extends Data.TaggedError("FileNotFound")<{
|
|
505
|
+
path: string;
|
|
506
|
+
}> {}
|
|
507
|
+
|
|
508
|
+
class PermissionDeniedError extends Data.TaggedError("PermissionDenied")<{
|
|
509
|
+
path: string;
|
|
510
|
+
operation: "read" | "write" | "execute";
|
|
511
|
+
}> {}
|
|
512
|
+
|
|
513
|
+
// Business logic errors
|
|
514
|
+
class ConfigurationError extends Data.TaggedError("ConfigurationError")<{
|
|
515
|
+
field: string;
|
|
516
|
+
message: string;
|
|
517
|
+
}> {}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### 2. Structured Error Data for Programmatic Handling
|
|
521
|
+
|
|
522
|
+
Include machine-readable data in errors:
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
class ProcessingError extends Data.TaggedError("ProcessingError")<{
|
|
526
|
+
// Human-readable
|
|
527
|
+
message: string;
|
|
528
|
+
|
|
529
|
+
// Machine-readable for programmatic handling
|
|
530
|
+
code: "PARSE_FAILED" | "VALIDATION_FAILED" | "TRANSFORM_FAILED";
|
|
531
|
+
file?: string;
|
|
532
|
+
line?: number;
|
|
533
|
+
column?: number;
|
|
534
|
+
|
|
535
|
+
// For debugging
|
|
536
|
+
cause?: unknown;
|
|
537
|
+
}> {}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 3. Exit Codes Based on Error Type
|
|
541
|
+
|
|
542
|
+
Map errors to appropriate exit codes:
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
import { Effect, Exit, Cause } from "effect";
|
|
546
|
+
|
|
547
|
+
const EXIT_CODES = {
|
|
548
|
+
SUCCESS: 0,
|
|
549
|
+
GENERAL_ERROR: 1,
|
|
550
|
+
INVALID_ARGUMENT: 2,
|
|
551
|
+
FILE_NOT_FOUND: 3,
|
|
552
|
+
PERMISSION_DENIED: 4,
|
|
553
|
+
CONFIGURATION_ERROR: 5,
|
|
554
|
+
NETWORK_ERROR: 6,
|
|
555
|
+
INTERNAL_ERROR: 127,
|
|
556
|
+
} as const;
|
|
557
|
+
|
|
558
|
+
const getExitCode = (cause: Cause.Cause<AppError>): number => {
|
|
559
|
+
if (Cause.isFailType(cause)) {
|
|
560
|
+
const error = cause.error;
|
|
561
|
+
switch (error._tag) {
|
|
562
|
+
case "InvalidArgument":
|
|
563
|
+
case "MissingRequiredOption":
|
|
564
|
+
return EXIT_CODES.INVALID_ARGUMENT;
|
|
565
|
+
case "FileNotFound":
|
|
566
|
+
return EXIT_CODES.FILE_NOT_FOUND;
|
|
567
|
+
case "PermissionDenied":
|
|
568
|
+
return EXIT_CODES.PERMISSION_DENIED;
|
|
569
|
+
case "ConfigurationError":
|
|
570
|
+
return EXIT_CODES.CONFIGURATION_ERROR;
|
|
571
|
+
case "NetworkError":
|
|
572
|
+
return EXIT_CODES.NETWORK_ERROR;
|
|
573
|
+
default:
|
|
574
|
+
return EXIT_CODES.GENERAL_ERROR;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// Defects get internal error code
|
|
578
|
+
return EXIT_CODES.INTERNAL_ERROR;
|
|
579
|
+
};
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### 4. User-Friendly Error Formatting
|
|
583
|
+
|
|
584
|
+
Format errors for human consumption at the application boundary:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
import { Effect, Console } from "effect";
|
|
588
|
+
|
|
589
|
+
// Centralized error formatter
|
|
590
|
+
const formatErrorForUser = (error: AppError): string => {
|
|
591
|
+
switch (error._tag) {
|
|
592
|
+
case "FileNotFound":
|
|
593
|
+
return `Error: File not found: ${error.path}\n\nPlease check the path and try again.`;
|
|
594
|
+
|
|
595
|
+
case "InvalidArgument":
|
|
596
|
+
return (
|
|
597
|
+
`Error: Invalid value for '${error.argument}'\n` +
|
|
598
|
+
` Expected: ${error.expected}\n` +
|
|
599
|
+
` Received: ${error.received}`
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
case "ConfigurationError":
|
|
603
|
+
return `Configuration error in '${error.field}': ${error.message}`;
|
|
604
|
+
|
|
605
|
+
case "PermissionDenied":
|
|
606
|
+
return `Permission denied: Cannot ${error.operation} '${error.path}'`;
|
|
607
|
+
|
|
608
|
+
default:
|
|
609
|
+
return `Error: ${error.message}`;
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Apply formatting at the top level
|
|
614
|
+
const runCLI = (program: Effect.Effect<void, AppError>) =>
|
|
615
|
+
program.pipe(
|
|
616
|
+
Effect.catchAll((error) =>
|
|
617
|
+
Console.error(formatErrorForUser(error)).pipe(
|
|
618
|
+
Effect.flatMap(() => Effect.fail(error)), // Re-fail for exit code
|
|
619
|
+
),
|
|
620
|
+
),
|
|
621
|
+
);
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### 5. Separate Error Presentation from Error Classes
|
|
625
|
+
|
|
626
|
+
Keep error classes clean - format at the boundary:
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
// Good: Error class is pure data
|
|
630
|
+
class ParseError extends Data.TaggedError("ParseError")<{
|
|
631
|
+
file: string;
|
|
632
|
+
line: number;
|
|
633
|
+
column: number;
|
|
634
|
+
message: string;
|
|
635
|
+
}> {}
|
|
636
|
+
|
|
637
|
+
// Format separately at the CLI boundary
|
|
638
|
+
const formatParseError = (error: ParseError): string =>
|
|
639
|
+
`Parse error in ${error.file}:${error.line}:${error.column}\n` +
|
|
640
|
+
` ${error.message}`;
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 6. Preserve Error Context Through Transformations
|
|
644
|
+
|
|
645
|
+
When transforming errors, preserve the original cause:
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
class HighLevelError extends Data.TaggedError("HighLevelError")<{
|
|
649
|
+
operation: string;
|
|
650
|
+
cause: unknown;
|
|
651
|
+
}> {}
|
|
652
|
+
|
|
653
|
+
const operation = lowLevelEffect.pipe(
|
|
654
|
+
Effect.mapError(
|
|
655
|
+
(error) =>
|
|
656
|
+
new HighLevelError({
|
|
657
|
+
operation: "user fetch",
|
|
658
|
+
cause: error, // Preserve original error
|
|
659
|
+
}),
|
|
660
|
+
),
|
|
661
|
+
);
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### 7. Use Effect's Built-in Terminal Service
|
|
665
|
+
|
|
666
|
+
For CLI output, prefer Effect's Terminal service:
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
import { Effect, Terminal } from "@effect/platform";
|
|
670
|
+
|
|
671
|
+
const displayError = (error: AppError) =>
|
|
672
|
+
Effect.gen(function* () {
|
|
673
|
+
const terminal = yield* Terminal.Terminal;
|
|
674
|
+
yield* terminal.display(formatErrorForUser(error));
|
|
675
|
+
});
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## Anti-patterns to Avoid
|
|
681
|
+
|
|
682
|
+
### Anti-pattern 1: console.error Inside Error Classes
|
|
683
|
+
|
|
684
|
+
**Bad:**
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
// DON'T: Side effects in error constructors
|
|
688
|
+
class BadError extends Data.TaggedError("BadError")<{
|
|
689
|
+
message: string;
|
|
690
|
+
}> {
|
|
691
|
+
constructor(props: { message: string }) {
|
|
692
|
+
super(props);
|
|
693
|
+
console.error(`Error: ${props.message}`); // Side effect!
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Why it's bad:**
|
|
699
|
+
|
|
700
|
+
- Mixes presentation with data
|
|
701
|
+
- Cannot control when/how errors are displayed
|
|
702
|
+
- Breaks referential transparency
|
|
703
|
+
- Makes testing difficult
|
|
704
|
+
- Cannot format differently for different contexts (CLI vs JSON API)
|
|
705
|
+
|
|
706
|
+
**Good:**
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
// DO: Keep errors as pure data
|
|
710
|
+
class GoodError extends Data.TaggedError("GoodError")<{
|
|
711
|
+
message: string;
|
|
712
|
+
}> {}
|
|
713
|
+
|
|
714
|
+
// Format at the application boundary
|
|
715
|
+
const program = myEffect.pipe(
|
|
716
|
+
Effect.catchAll((error) => Console.error(formatError(error))),
|
|
717
|
+
);
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### Anti-pattern 2: Converting Typed Errors to Generic Error Too Early
|
|
721
|
+
|
|
722
|
+
**Bad:**
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
// DON'T: Lose type information early
|
|
726
|
+
const fetchUser = (id: string) =>
|
|
727
|
+
Effect.tryPromise({
|
|
728
|
+
try: () => api.getUser(id),
|
|
729
|
+
catch: (e) => new Error(String(e)), // Lost all context!
|
|
730
|
+
});
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
**Good:**
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
// DO: Preserve specific error types
|
|
737
|
+
const fetchUser = (id: string) =>
|
|
738
|
+
Effect.tryPromise({
|
|
739
|
+
try: () => api.getUser(id),
|
|
740
|
+
catch: (e) => {
|
|
741
|
+
if (e instanceof Response && e.status === 404) {
|
|
742
|
+
return new UserNotFoundError({ userId: id });
|
|
743
|
+
}
|
|
744
|
+
if (e instanceof Response && e.status === 401) {
|
|
745
|
+
return new UnauthorizedError({ resource: `user/${id}` });
|
|
746
|
+
}
|
|
747
|
+
return new NetworkError({
|
|
748
|
+
message: String(e),
|
|
749
|
+
endpoint: `/users/${id}`,
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Anti-pattern 3: Mixing Thrown Exceptions with Effect Errors
|
|
756
|
+
|
|
757
|
+
**Bad:**
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// DON'T: Throw inside Effect
|
|
761
|
+
const process = Effect.sync(() => {
|
|
762
|
+
if (invalidCondition) {
|
|
763
|
+
throw new Error("Invalid!"); // Becomes a defect, not tracked!
|
|
764
|
+
}
|
|
765
|
+
return result;
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Good:**
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
// DO: Use Effect.fail for expected errors
|
|
773
|
+
const process = Effect.gen(function* () {
|
|
774
|
+
if (invalidCondition) {
|
|
775
|
+
return yield* Effect.fail(new ValidationError({ reason: "invalid" }));
|
|
776
|
+
}
|
|
777
|
+
return result;
|
|
778
|
+
});
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Anti-pattern 4: Swallowing Errors
|
|
782
|
+
|
|
783
|
+
**Bad:**
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
// DON'T: Silently ignore errors
|
|
787
|
+
const risky = effect.pipe(
|
|
788
|
+
Effect.catchAll(() => Effect.succeed(undefined)), // Error vanished!
|
|
789
|
+
);
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
**Good:**
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
// DO: Log or preserve error information
|
|
796
|
+
const risky = effect.pipe(
|
|
797
|
+
Effect.catchAll((error) =>
|
|
798
|
+
Effect.logWarning(`Operation failed: ${error._tag}`).pipe(
|
|
799
|
+
Effect.map(() => defaultValue),
|
|
800
|
+
),
|
|
801
|
+
),
|
|
802
|
+
);
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### Anti-pattern 5: Losing Error Context in Transformations
|
|
806
|
+
|
|
807
|
+
**Bad:**
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
// DON'T: Throw away the original error
|
|
811
|
+
const mapped = effect.pipe(
|
|
812
|
+
Effect.mapError(() => new GenericError({ message: "Something failed" })),
|
|
813
|
+
);
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Good:**
|
|
817
|
+
|
|
818
|
+
```typescript
|
|
819
|
+
// DO: Preserve the cause chain
|
|
820
|
+
const mapped = effect.pipe(
|
|
821
|
+
Effect.mapError(
|
|
822
|
+
(original) =>
|
|
823
|
+
new HighLevelError({
|
|
824
|
+
message: "Operation failed",
|
|
825
|
+
cause: original, // Keep the original
|
|
826
|
+
}),
|
|
827
|
+
),
|
|
828
|
+
);
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Anti-pattern 6: Not Using Tagged Errors
|
|
832
|
+
|
|
833
|
+
**Bad:**
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
// DON'T: Plain objects without tags
|
|
837
|
+
const fail = Effect.fail({ message: "error", code: 123 });
|
|
838
|
+
// Cannot use catchTag, hard to distinguish error types
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
**Good:**
|
|
842
|
+
|
|
843
|
+
```typescript
|
|
844
|
+
// DO: Use TaggedError for all domain errors
|
|
845
|
+
class ApiError extends Data.TaggedError("ApiError")<{
|
|
846
|
+
message: string;
|
|
847
|
+
code: number;
|
|
848
|
+
}> {}
|
|
849
|
+
|
|
850
|
+
const fail = Effect.fail(new ApiError({ message: "error", code: 123 }));
|
|
851
|
+
// Now catchTag works, types are clear
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Anti-pattern 7: Over-catching with catchAll
|
|
855
|
+
|
|
856
|
+
**Bad:**
|
|
857
|
+
|
|
858
|
+
```typescript
|
|
859
|
+
// DON'T: Catch everything at low levels
|
|
860
|
+
const fetchData = fetchFromApi().pipe(
|
|
861
|
+
Effect.catchAll(() => Effect.succeed(defaultData)), // Hides ALL errors
|
|
862
|
+
);
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
**Good:**
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
// DO: Handle specific errors, let others propagate
|
|
869
|
+
const fetchData = fetchFromApi().pipe(
|
|
870
|
+
Effect.catchTag(
|
|
871
|
+
"NetworkError",
|
|
872
|
+
(e) =>
|
|
873
|
+
e.statusCode === 404 ? Effect.succeed(defaultData) : Effect.fail(e), // Re-fail for other network errors
|
|
874
|
+
),
|
|
875
|
+
// Other errors propagate up
|
|
876
|
+
);
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### Anti-pattern 8: Using Defects for Expected Errors
|
|
880
|
+
|
|
881
|
+
**Bad:**
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
// DON'T: Use die for business logic errors
|
|
885
|
+
const validateAge = (age: number) =>
|
|
886
|
+
age < 0
|
|
887
|
+
? Effect.die(new Error("Age cannot be negative")) // Wrong!
|
|
888
|
+
: Effect.succeed(age);
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**Good:**
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
// DO: Use fail for expected/recoverable errors
|
|
895
|
+
class InvalidAgeError extends Data.TaggedError("InvalidAge")<{
|
|
896
|
+
age: number;
|
|
897
|
+
}> {}
|
|
898
|
+
|
|
899
|
+
const validateAge = (age: number) =>
|
|
900
|
+
age < 0
|
|
901
|
+
? Effect.fail(new InvalidAgeError({ age })) // Correct!
|
|
902
|
+
: Effect.succeed(age);
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
---
|
|
906
|
+
|
|
907
|
+
## Sources
|
|
908
|
+
|
|
909
|
+
### Official Documentation
|
|
910
|
+
|
|
911
|
+
- [Effect Documentation - Why Effect?](https://effect.website/docs/getting-started/why-effect/)
|
|
912
|
+
- [Effect Documentation - The Effect Type](https://effect.website/docs/getting-started/the-effect-type/)
|
|
913
|
+
- [Effect Documentation - Expected Errors](https://effect.website/docs/error-management/expected-errors/)
|
|
914
|
+
- [Effect Documentation - Unexpected Errors](https://effect.website/docs/error-management/unexpected-errors/)
|
|
915
|
+
- [Effect Documentation - Yieldable Errors](https://effect.website/docs/error-management/yieldable-errors/)
|
|
916
|
+
- [Effect Documentation - Error Channel Operations](https://effect.website/docs/error-management/error-channel-operations/)
|
|
917
|
+
- [Effect Documentation - Fallback](https://effect.website/docs/error-management/fallback/)
|
|
918
|
+
- [Effect Documentation - Retrying](https://effect.website/docs/error-management/retrying/)
|
|
919
|
+
- [Effect Documentation - Cause](https://effect.website/docs/data-types/cause/)
|
|
920
|
+
- [Effect Documentation - Data](https://effect.website/docs/data-types/data/)
|
|
921
|
+
- [Effect Documentation - Terminal](https://effect.website/docs/platform/terminal/)
|
|
922
|
+
- [Effect Documentation - Error Formatters](https://effect.website/docs/schema/error-formatters/)
|
|
923
|
+
|
|
924
|
+
### Community Resources
|
|
925
|
+
|
|
926
|
+
- [TypeOnce - Define Errors with TaggedError](https://www.typeonce.dev/course/effect-beginners-complete-getting-started/type-safe-error-handling-with-effect/define-errors-with-taggederror)
|
|
927
|
+
- [Intro to Effect Part 2: Handling Errors](https://ybogomolov.me/02-effect-handling-errors)
|
|
928
|
+
- [How to Effect TS? - Best Practices](https://dtech.vision/blog/how-to-effect-ts-best-practices/)
|
|
929
|
+
- [Effect Patterns Repository](https://github.com/PaulJPhilp/EffectPatterns)
|
|
930
|
+
- [Exploring Effect in TypeScript - Tweag](https://www.tweag.io/blog/2024-11-07-typescript-effect/)
|
|
931
|
+
- [TypeScript Errors and Effect](https://davidmyno.rs/blog/typed-errors-and-effect/)
|
|
932
|
+
- [DeepWiki - Error Handling with Cause](https://deepwiki.com/Effect-TS/effect/2.5-error-handling-with-cause-either-and-option)
|
|
933
|
+
|
|
934
|
+
### CLI Framework
|
|
935
|
+
|
|
936
|
+
- [@effect/cli npm package](https://www.npmjs.com/package/@effect/cli)
|
|
937
|
+
- [DeepWiki - CLI Framework](https://deepwiki.com/Effect-TS/effect/8.1-cli-framework)
|
|
938
|
+
- [Effect CLI README](https://github.com/Effect-TS/effect/blob/main/packages/cli/README.md)
|
|
939
|
+
|
|
940
|
+
---
|
|
941
|
+
|
|
942
|
+
_Document created: 2026-01-22_
|
|
943
|
+
_Research scope: Effect's errors-as-values pattern for type-safe error handling_
|