Skip to content

Possibility to handle remaining errors in catchTags #4784

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: next-minor
Choose a base branch
from
31 changes: 31 additions & 0 deletions .changeset/soft-bears-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"effect": minor
---

Possibility to recover from unhandled errors in `catchTags`.

This addition introduces the possibility to define a function as a second parameter that gets the error type which was unhandled in the map before.

```ts
import { Data, Effect } from "effect"

class A extends Data.TaggedError("A") {}
class B extends Data.TaggedError("B") {}
class C extends Data.TaggedError("C") {}
class D extends Data.TaggedError("D") {}

const program = Effect.gen(function* () {
const types = [A, B, C, D]
const Exception = types[Math.floor(Math.random() * types.length)]

return yield* new Exception()
}).pipe(
Effect.catchTags(
{
A: (a) => Effect.succeed(a._tag),
B: (b) => Effect.succeed(b._tag)
},
(remaining) => Effect.succeed(`remaining: ${remaining._tag}`) // C | D
)
)
```
103 changes: 103 additions & 0 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3904,9 +3904,11 @@ export const catchTag: {
* const program = Effect.gen(function* () {
* const n1 = yield* Random.next
* const n2 = yield* Random.next
*
* if (n1 < 0.5) {
* yield* Effect.fail(new HttpError())
* }
*
* if (n2 < 0.5) {
* yield* Effect.fail(new ValidationError())
* }
Expand All @@ -3925,6 +3927,52 @@ export const catchTag: {
* )
* ```
*
* **Example** (Handling Tagged Error Types and Remaining Errors)
*
* ```ts
* import { Effect, Random } from "effect"
*
* class HttpError {
* readonly _tag = "HttpError"
* }
*
* class ValidationError {
* readonly _tag = "ValidationError"
* }
*
* class AnotherValidationError {
* readonly _tag = "ValidationError"
* }
*
* // ┌─── Effect<string, HttpError | ValidationError | AnotherValidationError, never>
* // ▼
* const program = Effect.gen(function* () {
* const n1 = yield* Random.next
* const n2 = yield* Random.next
* const n3 = yield* Random.next
*
* if (n1 < 0.5) {
* yield* Effect.fail(new HttpError())
* }
* if (n2 < 0.5) {
* yield* Effect.fail(new ValidationError())
* }
*
* return "some result"
* })
*
* // ┌─── Effect<string, never, never>
* // ▼
* const recovered = program.pipe(
* Effect.catchTags({
* HttpError: (_HttpError) =>
* Effect.succeed(`Recovering from HttpError`),
* ValidationError: (_ValidationError) =>
* Effect.succeed(`Recovering from ValidationError`)
* }, remaining => Effect.succeed(remaining._tag)) // AnotherValidationError
* )
* ```
*
* @since 2.0.0
* @category Error handling
*/
Expand Down Expand Up @@ -3952,6 +4000,33 @@ export const catchTags: {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect<any, any, infer R> ? R : never
}[keyof Cases]
>
<
E,
Cases extends
& { [K in Extract<E, { _tag: string }>["_tag"]]?: (error: Extract<E, { _tag: K }>) => Effect<any, any, any> }
& (unknown extends E ? {} : { [K in Exclude<keyof Cases, Extract<E, { _tag: string }>["_tag"]>]: never }),
OnOther extends (error: Exclude<E, { _tag: keyof Cases }>) => Effect<any, any, any>
>(
cases: Cases,
onOther: OnOther
): <A, R>(
self: Effect<A, E, R>
) => Effect<
| A
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect<infer A1, any, any> ? A1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect<infer A2, any, any> ? A2 : never),
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect<any, infer E1, any> ? E1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect<any, infer E2, any> ? E2 : never),
| R
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect<any, any, infer R1> ? R1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect<any, any, infer R2> ? R2 : never)
>
<
R,
E,
Expand All @@ -3976,6 +4051,34 @@ export const catchTags: {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect<any, any, infer R> ? R : never
}[keyof Cases]
>
<
E,
A,
R,
Cases extends
& { [K in Extract<E, { _tag: string }>["_tag"]]?: (error: Extract<E, { _tag: K }>) => Effect<any, any, any> }
& (unknown extends E ? {} : { [K in Exclude<keyof Cases, Extract<E, { _tag: string }>["_tag"]>]: never }),
OnOther extends (error: Exclude<E, { _tag: keyof Cases }>) => Effect<any, any, any>
>(
self: Effect<A, E, R>,
cases: Cases,
onOther: OnOther
): Effect<
| A
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect<infer A1, any, any> ? A1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect<infer A2, any, any> ? A2 : never),
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect<any, infer E1, any> ? E1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect<any, infer E2, any> ? E2 : never),
| R
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect<any, any, infer R1> ? R1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect<any, any, infer R2> ? R2 : never)
>
} = effect.catchTags

/**
Expand Down
120 changes: 95 additions & 25 deletions packages/effect/src/internal/core-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,62 +257,132 @@ export const catchTag = dual<
export const catchTags: {
<
E,
Cases extends (E extends { _tag: string } ? {
[K in E["_tag"]]+?: (error: Extract<E, { _tag: K }>) => Effect.Effect<any, any, any>
} :
{})
Cases
extends (E extends { _tag: string }
? { [K in E["_tag"]]+?: (error: Extract<E, { _tag: K }>) => Effect.Effect<any, any, any> }
: {})
>(
cases: Cases
): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<
| A
| {
[K in keyof Cases]: Cases[K] extends ((...args: Array<any>) => Effect.Effect<infer A, any, any>) ? A : never
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<infer A1, any, any> ? A1 : never
}[keyof Cases],
| Exclude<E, { _tag: keyof Cases }>
| {
[K in keyof Cases]: Cases[K] extends ((...args: Array<any>) => Effect.Effect<any, infer E, any>) ? E : never
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, infer E1, any> ? E1 : never
}[keyof Cases],
| R
| {
[K in keyof Cases]: Cases[K] extends ((...args: Array<any>) => Effect.Effect<any, any, infer R>) ? R : never
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, any, infer R1> ? R1 : never
}[keyof Cases]
>
<
E,
Cases
extends (E extends { _tag: string }
? { [K in E["_tag"]]+?: (error: Extract<E, { _tag: K }>) => Effect.Effect<any, any, any> }
: {}),
OnOther extends (error: Exclude<E, { _tag: keyof Cases }>) => Effect.Effect<any, any, any>
>(
cases: Cases,
onOther: OnOther
): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<
| A
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<infer A1, any, any> ? A1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect.Effect<infer A2, any, any> ? A2 : never),
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, infer E1, any> ? E1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect.Effect<any, infer E2, any> ? E2 : never),
| R
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, any, infer R1> ? R1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect.Effect<any, any, infer R2> ? R2 : never)
>
<
R,
E,
A,
Cases extends (E extends { _tag: string } ? {
[K in E["_tag"]]+?: (error: Extract<E, { _tag: K }>) => Effect.Effect<any, any, any>
} :
{})
Cases
extends (E extends { _tag: string }
? { [K in E["_tag"]]+?: (error: Extract<E, { _tag: K }>) => Effect.Effect<any, any, any> }
: {})
>(
self: Effect.Effect<A, E, R>,
cases: Cases
): Effect.Effect<
| A
| {
[K in keyof Cases]: Cases[K] extends ((...args: Array<any>) => Effect.Effect<infer A, any, any>) ? A : never
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<infer A1, any, any> ? A1 : never
}[keyof Cases],
| Exclude<E, { _tag: keyof Cases }>
| {
[K in keyof Cases]: Cases[K] extends ((...args: Array<any>) => Effect.Effect<any, infer E, any>) ? E : never
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, infer E1, any> ? E1 : never
}[keyof Cases],
| R
| {
[K in keyof Cases]: Cases[K] extends ((...args: Array<any>) => Effect.Effect<any, any, infer R>) ? R : never
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, any, infer R1> ? R1 : never
}[keyof Cases]
>
} = dual(2, (self, cases) => {
let keys: Array<string>
return core.catchIf(
self,
(e): e is { readonly _tag: string } => {
keys ??= Object.keys(cases)
return Predicate.hasProperty(e, "_tag") && Predicate.isString(e["_tag"]) && keys.includes(e["_tag"])
},
(e) => cases[e["_tag"]](e)
)
})
<
R,
E,
A,
Cases
extends (E extends { _tag: string }
? { [K in E["_tag"]]+?: (error: Extract<E, { _tag: K }>) => Effect.Effect<any, any, any> }
: {}),
OnOther extends (error: Exclude<E, { _tag: keyof Cases }>) => Effect.Effect<any, any, any>
>(
self: Effect.Effect<A, E, R>,
cases: Cases,
onOther: OnOther
): Effect.Effect<
| A
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<infer A1, any, any> ? A1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect.Effect<infer A2, any, any> ? A2 : never),
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, infer E1, any> ? E1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect.Effect<any, infer E2, any> ? E2 : never),
| R
| {
[K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, any, infer R1> ? R1 : never
}[keyof Cases]
| (ReturnType<OnOther> extends Effect.Effect<any, any, infer R2> ? R2 : never)
>
} = dual(
(args) => core.isEffect(args[0]),
(self, cases, onOther?) => {
let keys: Array<string>

if (onOther) {
return core.catchAll(self, (e) => {
keys ??= Object.keys(cases)
if (Predicate.hasProperty(e, "_tag") && Predicate.isString(e["_tag"]) && keys.includes(e["_tag"])) {
return cases[e["_tag"]](e)
}

return onOther(e)
})
}

return core.catchIf(
self,
(e): e is { readonly _tag: string } => {
keys ??= Object.keys(cases)
return Predicate.hasProperty(e, "_tag") && Predicate.isString(e["_tag"]) && keys.includes(e["_tag"])
},
(e) => cases[e["_tag"]](e)
)
}
)

/* @internal */
export const cause = <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<Cause.Cause<E>, never, R> =>
Expand Down
21 changes: 21 additions & 0 deletions packages/effect/test/Effect/error-handling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ describe("Effect", () => {
const result = yield* (Effect.catchTags(effect, {
ErrorA: (e) => Effect.succeed(e)
}))

deepStrictEqual(result, { _tag: "ErrorA" })
}))
it.effect("catchTags - does not recover from one of several tagged errors", () =>
Expand Down Expand Up @@ -316,6 +317,26 @@ describe("Effect", () => {
}))
deepStrictEqual(result, { _tag: "ErrorB" })
}))
it.effect("catchTags - recovers from tagged errors and remaining errors", () =>
Effect.gen(function*() {
interface ErrorA {
readonly _tag: "ErrorA"
}
interface ErrorB {
readonly _tag: "ErrorB"
}
interface ErrorC {
readonly _tag: "ErrorC"
}

const effect: Effect.Effect<never, ErrorA | ErrorB | ErrorC, never> = Effect.fail({ _tag: "ErrorC" })

const result = yield* (Effect.catchTags(effect, {
ErrorA: (e) => Effect.succeed(e),
ErrorB: (e) => Effect.succeed(e)
}, (remaining) => Effect.succeed(remaining)))
deepStrictEqual(result, { _tag: "ErrorC" })
}))
it.effect("fold - sandbox -> terminate", () =>
Effect.gen(function*() {
const result = yield* pipe(
Expand Down
Loading