You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every error so far is one a caller can respond to. These are expected errors. But `never` does not mean the code cannot fail.
661
+
Every error so far is one a caller can handle. These are expected errors. But `never` does not mean the code cannot fail.
662
662
663
-
Some conditions must always hold, or the payment step charges a customer wrong. Each is an invariant:
663
+
One condition must always hold: an authorization is captured once, never twice. This condition is an invariant. If it breaks, the payment step charges the customer twice.
664
664
665
-
- An authorization is captured once, never twice.
666
-
- The provider returns a status its contract promises.
665
+
When an invariant breaks, no handler can respond. The safe action is to stop and report.
667
666
668
-
When one breaks, no handler can respond, so the safe action is to stop and report.
667
+
Effect calls this failure an **unexpected error**, or a **defect**: an impossible state, a broken invariant, or a bug in third-party code.
669
668
670
-
Effect calls a failure like this an **unexpected error**, or a **defect**: an impossible state, a broken invariant, or a bug in third-party code.
669
+
A defect is not tracked in the `Error` type parameter. The runtime records it in a `Cause`.
671
670
672
-
Unlike an expected error, a defect is not tracked in the `Error` type parameter; the runtime records it in a `Cause`.
671
+
The capture step enforces this invariant. The ledger fixture starts with the payment already captured. So this run is the retry, and the invariant breaks.
673
672
674
-
The capture step enforces the first invariant. The ledger fixture starts with
675
-
the payment already captured, so this run is the retry, and the invariant
676
-
breaks.
677
-
678
-
No caller can undo a double capture, so a typed error would promise a handler
679
-
that cannot exist. The step dies instead: `Effect.die` sends the error to the
680
-
`Cause`, and the `Error` type parameter stays empty.
673
+
No caller can undo a double capture. A typed error would promise a handler that cannot exist. The step dies instead. `Effect.die` sends the error to the `Cause`, and the `Error` type parameter stays empty.
The signature promises `Effect.Effect<Authorization>` with no `Error` type
770
-
parameter, yet the double capture fails the run. The failure never entered the
771
-
type: it became a defect, and `Effect.catchDefect` caught it. Without that
772
-
handler, `Effect.runPromise` rejects. Unknown defects stay fatal, so the handler
773
-
re-raises them with `Effect.die`.
762
+
The signature promises `Effect.Effect<Authorization>` with no `Error` type parameter. Yet the double capture fails the run. The failure never entered the type. It became a defect, and `Effect.catchDefect` caught it. Without that handler, `Effect.runPromise` rejects. Unknown defects stay fatal, so the handler re-raises them with `Effect.die`.
774
763
775
764
### Remove errors no caller can handle from the type
776
765
777
-
`Effect.orDie` is a modeling decision: no caller can do anything with these
778
-
errors, so stop tracking them. Every typed error becomes a defect, and the
779
-
`Error` type parameter disappears.
766
+
`Effect.orDie` is a modeling decision. No caller can handle these errors, so stop tracking them. Every typed error becomes a defect, and the `Error` type parameter disappears.
780
767
781
-
Apply it at the boundary between your Effect code and its callers: inside, the
782
-
code still fails with typed errors, but the signature the caller sees has none
783
-
left.
768
+
Apply it at the boundary between your Effect code and its callers. Inside, the code still fails with typed errors. The signature the caller sees has none left.
784
769
785
770
```ts twoslash import.meta.vitest
786
771
import { Effect, Data } from"effect"
@@ -834,25 +819,20 @@ try {
834
819
}
835
820
```
836
821
837
-
A nightly reconciliation job is one such boundary: when the card is expired, the
838
-
job crashes and alerts whoever owns it. The failure crossed the boundary as a
839
-
defect, so `Effect.runPromise` rejected with the `ExpiredCardError` itself.
822
+
A nightly reconciliation job is one such boundary. When the card is expired, the job crashes and alerts its owner. The failure crossed the boundary as a defect. So `Effect.runPromise` rejected with the `ExpiredCardError` itself.
840
823
841
824
### Handle defects at the application boundary
842
825
843
-
A defect is a bug. A handler cannot fix it. But the boundary decides what
844
-
happens next, once, in one place: the job runner, the HTTP handler, the batch
845
-
loop. For each defect, ask:
826
+
A defect is a bug. A handler cannot fix it. But the boundary decides what happens next, once, in one place: the job runner, the HTTP handler, the batch loop. For each defect, choose:
846
827
847
-
1.Could a caller respond to this? Then it is not a bug. Model it as a typed
848
-
error: a class with a `_tag`, named in the `Error` type parameter.
849
-
1. Is it a bug? Report it, and let the program terminate.
850
-
1. Can the program continue without it? Catch it, report it, and go on.
828
+
1.Report it, and let the program stop.
829
+
2. Or catch it, report it, and let the program continue.
830
+
831
+
If a caller can respond to the failure, it is not a defect. Model it as a typed error instead.
851
832
852
833
Inside the program, defects stay fatal. Only the boundary decides.
853
834
854
-
`Effect.catchDefect` handles defects only. The next example shows that typed
855
-
errors pass through unchanged.
835
+
`Effect.catchDefect` handles defects only. The next example shows that typed errors pass through unchanged.
856
836
857
837
```ts twoslash import.meta.vitest
858
838
import { Effect, Data } from"effect"
@@ -866,7 +846,7 @@ class ExpiredCardError extends Data.TaggedError("ExpiredCardError") {}
The `catchDefect` handler never ran. `catch (failure)`, not `catch (defect)`:
917
-
the typed error was never a defect. It reached `runPromise` untouched,
918
-
exactly as its type said, with all three members still in the `Error` type
919
-
parameter.
896
+
The `catchDefect` handler never ran. The result is `catch (failure)`, not `catch (defect)`. The typed error was never a defect. It reached `runPromise` unchanged, exactly as its type said, with all three members still in the `Error` type parameter.
920
897
921
898
A rule of thumb for the whole section:
922
899
@@ -927,18 +904,18 @@ A rule of thumb for the whole section:
927
904
928
905
<Asidetype="note"title="Checkpoint">
929
906
"Did I cover every case?" has two answers now. For the errors you modeled, the
930
-
`Error` type parameter answers: it shrinks as you handle them. For the ones
931
-
you didn't, the boundary answers:`catchDefect` sees them at the edge, where
932
-
you report them and let the program terminate.
907
+
`Error` type parameter answers. It shrinks as you handle them. For the errors
908
+
you did not model, the boundary answers.`catchDefect` sees them at the edge,
909
+
where you report them and let the program stop.
933
910
</Aside>
934
911
935
912
## Let's review what we built
936
913
937
914
You can now do five things. Each one maps to an operator you used.
938
915
939
-
- Read an `Effect` signature as a contract. The `Success` type parameter names the result, and the`Error` type parameter names every error a caller can handle.
940
-
- Make each error visible to TypeScript with `Data.TaggedError`, so TypeScript tracks it instead of losing it as `unknown` in a `catch`.
941
-
- Return errors instead of throwing them. Inside `Effect.gen`, fail with `return yield* new XError()`, and the`Error` type parameter tracks each one.
916
+
- Read an `Effect` signature as a contract. The `Success` type parameter names the result. The`Error` type parameter names every error a caller can handle.
917
+
- Make each error visible to TypeScript with `Data.TaggedError`. TypeScript tracks it instead of losing it as `unknown` in a `catch`.
918
+
- Return errors instead of throwing them. Inside `Effect.gen`, fail with `return yield* new XError()`. The`Error` type parameter tracks each one.
942
919
- Handle one `_tag` with `Effect.catchTag`, and many with `Effect.catchTags`. Each handled tag leaves the `Error` union. An empty union is `never`.
943
920
- Keep defects apart from typed errors. `Effect.die` records a defect in the `Cause`. `Effect.catchDefect` handles defects at the boundary. `Effect.orDie` converts tracked errors you no longer handle.
0 commit comments