@@ -830,176 +830,6 @@ never entered the `Error` type parameter: it became a defect.
830830with the error. Unknown defects stay fatal, so the handler re-raises them with
831831` Effect.die ` .
832832
833- ### Typed recovery never catches a defect
834-
835- ` Effect.catchTags ` from the previous section handles every modeled failure.
836- Does it also handle defects? Give the job both steps, authorize then capture,
837- and handle every tag:
838-
839- ``` ts twoslash import.meta.vitest
840- import { Effect , Data } from " effect"
841-
842- interface Authorization {
843- readonly authorizationId: string
844- readonly status: " authorized" | " declined"
845- }
846-
847- class NetworkError extends Data .TaggedError (" NetworkError" )<{
848- readonly cause? : unknown
849- }> {}
850- class PaymentDeclinedError extends Data .TaggedError (" PaymentDeclinedError" ) {}
851- class ParseError extends Data .TaggedError (" ParseError" )<{
852- readonly cause? : unknown
853- }> {}
854- class DoubleCaptureError extends Data .TaggedError (" DoubleCaptureError" )<{
855- readonly authorizationId: string
856- }> {}
857-
858- function isAuthorization(body : unknown ): body is Authorization {
859- const r = body as Authorization
860- return (
861- (r ?.status === " authorized" || r ?.status === " declined" ) &&
862- typeof r ?.authorizationId === " string"
863- )
864- }
865-
866- type PaymentToken = " tok_visa" | " tok_declined" | " tok_timeout"
867-
868- const authorizePayment = async (token : PaymentToken ): Promise <unknown > => {
869- if (token === " tok_timeout" ) {
870- throw new Error (" Provider unreachable" )
871- }
872- if (token === " tok_declined" ) {
873- return { authorizationId: " pauth_123" , status: " declined" }
874- }
875- return { authorizationId: " pauth_123" , status: " authorized" }
876- }
877-
878- const processPayment = (token : PaymentToken ) =>
879- Effect .gen (function * () {
880- const response = yield * Effect .tryPromise ({
881- try : () => authorizePayment (token ),
882- catch : (cause ) => new NetworkError ({ cause }),
883- })
884-
885- if (! isAuthorization (response )) {
886- return yield * new ParseError ({})
887- }
888-
889- if (response .status === " declined" ) {
890- return yield * new PaymentDeclinedError ()
891- }
892-
893- return response
894- })
895-
896- const captured = new Set ([" pauth_123" ])
897-
898- const capturePayment = (
899- authorization : Authorization ,
900- ): Effect .Effect <Authorization > =>
901- Effect .gen (function * () {
902- if (captured .has (authorization .authorizationId )) {
903- return yield * Effect .die (
904- new DoubleCaptureError ({
905- authorizationId: authorization .authorizationId ,
906- }),
907- )
908- }
909- captured .add (authorization .authorizationId )
910- return authorization
911- })
912-
913- const captureFlow = (token : PaymentToken ) =>
914- Effect .gen (function * () {
915- const authorization = yield * processPayment (token )
916- return yield * capturePayment (authorization )
917- })
918-
919- // ---cut---
920- // ┌─ Effect.Effect<string | Authorization, never>
921- // |
922- // ▼
923- const outcome = captureFlow (" tok_visa" ).pipe (
924- Effect .catchTags ({
925- NetworkError : () => Effect .succeed (" handled NetworkError" ),
926- ParseError : () => Effect .succeed (" handled ParseError" ),
927- PaymentDeclinedError : () => Effect .succeed (" handled PaymentDeclinedError" ),
928- }),
929- Effect .catchDefect (() => Effect .succeed (" the defect passed through" )),
930- )
931-
932- await Effect .runPromise (outcome ) // => "the defect passed through"
933- ```
934-
935- The table handles every tag, and the ` Error ` type parameter is ` never ` . The
936- defect still terminated the program: typed recovery never catches defects, so
937- only ` Effect.catchDefect ` saw it.
938-
939- ### Guard impossible data
940-
941- Whether a bug throws an error or you write it with ` Effect.die ` , it lands in
942- the same place: the ` Cause ` . ` Effect.die ` is the deliberate form, for the
943- invariants you check on purpose.
944-
945- The third invariant is a status the type says cannot exist. The problem:
946-
947- 1 . ` authorization.status ` has two members: ` "authorized" ` and ` "declined" ` .
948- 1 . Data from an untyped source, a parsed JSON body or a third-party response,
949- can carry anything at runtime.
950- 1 . A ` default ` arm in the switch guards the difference. The type says the arm
951- is unreachable, the runtime says otherwise.
952-
953- The arm casts before reading the status, because TypeScript considers it dead
954- code. ` Effect.die ` is what it returns: an exception you raise on purpose. It
955- never enters the ` Error ` type parameter.
956-
957- ``` ts twoslash import.meta.vitest
958- import { Effect } from " effect"
959-
960- interface Authorization {
961- readonly authorizationId: string
962- readonly status: " authorized" | " declined"
963- }
964-
965- // ┌─ Effect.Effect<string>: no `Error` type parameter
966- // ▼
967- const label = (authorization : Authorization ) => {
968- switch (authorization .status ) {
969- case " authorized" :
970- return Effect .succeed (` charged ${authorization .authorizationId } ` )
971- case " declined" :
972- return Effect .succeed (" no charge" )
973- default : {
974- // Unreachable per the type. Here for the runtime anyway.
975- const status = (authorization as { status: string }).status
976- return Effect .die (new Error (` Impossible status: ${status } ` ))
977- }
978- }
979- }
980-
981- // The data lies: a status the type says cannot exist.
982- const forged = {
983- authorizationId: " pauth_123" ,
984- status: " refunded" ,
985- } as unknown as Authorization
986-
987- const outcome = label (forged ).pipe (
988- Effect .catchDefect ((defect ) =>
989- defect instanceof Error
990- ? Effect .succeed (` terminated with a defect: ${defect .message } ` )
991- : Effect .die (defect ),
992- ),
993- )
994-
995- await Effect .runPromise (outcome ) // => "terminated with a defect: Impossible status: refunded"
996- ```
997-
998- The forged value stands in for the untyped source. The status is ` "refunded" ` :
999- the type says it cannot exist, and the runtime says otherwise. The impossible
1000- arm fired, and ` Effect.die ` terminated with a descriptive ` Error ` . Pass an
1001- ` Error ` with a useful message: it is the report ` catchDefect ` receives.
1002-
1003833### Stop tracking failures no caller can respond to
1004834
1005835` Effect.orDie ` is a modeling decision: no caller can do anything with these
@@ -1058,56 +888,6 @@ alerts whoever owns it. Nothing here is recoverable in place, so nothing is
1058888left to track. The failure crossed the boundary as a defect:
1059889` Effect.runPromise ` rejected with the ` PaymentDeclinedError ` itself.
1060890
1061- ### Promote one failure to a defect
1062-
1063- ` Effect.orDie ` promotes every failure. Promote one failure when it is
1064- a bug, not an expected outcome. Suppose the provider is your own service, and
1065- its contract guarantees the response shape. ` ParseError ` stops being a failure
1066- a caller can respond to: it is a broken promise. To promote one while keeping
1067- the rest tracked, catch the tag and die with it:
1068-
1069- ``` ts twoslash showLineNumbers=false
1070- import { Effect , Data } from " effect"
1071-
1072- interface Authorization {
1073- readonly authorizationId: string
1074- readonly status: " authorized" | " declined"
1075- }
1076-
1077- class NetworkError extends Data .TaggedError (" NetworkError" )<{
1078- readonly cause? : unknown
1079- }> {}
1080- class PaymentDeclinedError extends Data .TaggedError (" PaymentDeclinedError" ) {}
1081- class ParseError extends Data .TaggedError (" ParseError" )<{
1082- readonly cause? : unknown
1083- }> {}
1084-
1085- // Tutorial fixture: sandbox tokens make provider results reproducible.
1086- type PaymentToken = " tok_visa" | " tok_declined" | " tok_timeout"
1087-
1088- declare const processPayment: (
1089- token : PaymentToken ,
1090- ) => Effect .Effect <
1091- Authorization ,
1092- PaymentDeclinedError | NetworkError | ParseError
1093- >
1094-
1095- // ┌─ Effect.Effect<Authorization, PaymentDeclinedError | NetworkError>
1096- // | `ParseError` is now a defect; `PaymentDeclinedError` and `NetworkError` stay tracked
1097- // ▼
1098- const strictProcessPayment = (token : PaymentToken ) =>
1099- processPayment (token ).pipe (
1100- Effect .catchTag (" ParseError" , (error ) => Effect .die (error )),
1101- )
1102- ```
1103-
1104- Keep ` PaymentDeclinedError ` and ` NetworkError ` as failures:
1105-
1106- - A decline has a message for the customer.
1107- - A timeout has a retry.
1108-
1109- Promoting those would make them invisible to every caller.
1110-
1111891### Recover at the boundary
1112892
1113893A defect is a bug. A handler cannot fix it. But the boundary decides what
0 commit comments