From 3cac851ec541c0d6353a6db90e3e1ed99e1ff820 Mon Sep 17 00:00:00 2001 From: Mattia Crovero Date: Wed, 20 Aug 2025 12:46:01 +0200 Subject: [PATCH] fix: handling fails and defects correctly --- example/error.ts | 27 ++++++++++++++++++++++++ src/ReactCache.ts | 54 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 example/error.ts diff --git a/example/error.ts b/example/error.ts new file mode 100644 index 0000000..179fc48 --- /dev/null +++ b/example/error.ts @@ -0,0 +1,27 @@ +import { Data, Effect } from "effect" +import { reactCache } from "../src/ReactCache.js" + +class CustomError extends Data.TaggedError("CustomError") {} + +// Example of a function that yields an error string +function cachableFunctionWithError(id: string) { + return Effect.gen(function*() { + yield* Effect.log(`Attempting to fetch user ${id}`) + yield* Effect.sleep(1000) + // Simulate an error + return yield* new CustomError() + }) +} + +export const cachedFunctionWithError = reactCache(cachableFunctionWithError) + +const result = await Effect.runPromise( + Effect.gen(function*() { + const result = yield* cachedFunctionWithError("x").pipe( + Effect.catchTag("CustomError", () => Effect.succeed("error")) + ) + return result + }) +) + +console.log(result) diff --git a/src/ReactCache.ts b/src/ReactCache.ts index 0aacfe2..51e9b69 100644 --- a/src/ReactCache.ts +++ b/src/ReactCache.ts @@ -1,8 +1,19 @@ -import { Effect } from "effect" +import { Cause, Effect, Exit } from "effect" import type * as Context from "effect/Context" import type * as Scope from "effect/Scope" import { cache } from "react" +type CauseResult = { + error: E | undefined + defect: unknown +} + +type PromiseResult = { + success: A | undefined + error: E | undefined + defect: unknown +} + /** * @since 1.0.0 * @category type ids @@ -23,11 +34,33 @@ const runEffectFn = >( effect: (...args: Args) => Effect.Effect>, context: Context.Context>, ...args: Args -) => { +): Promise> => { const effectResult = effect(...args) const effectWithContext = Effect.provide(effectResult, context) - return Effect.runPromise(effectWithContext) + return Effect.runPromiseExit(effectWithContext).then((exit) => { + if (Exit.isSuccess(exit)) { + return { success: exit.value, error: undefined, defect: undefined } + } + if (Exit.isFailure(exit)) { + const cause = Cause.match(exit.cause, { + onEmpty: { error: undefined, defect: undefined } as CauseResult, + onFail: (error) => ({ error, defect: undefined }), + onDie: (defect) => ({ error: undefined, defect }), + onInterrupt: () => { + throw new Error("Interrupt cause not supported") + }, + onSequential: () => { + throw new Error("Sequential cause not supported") + }, + onParallel: () => { + throw new Error("Parallel cause not supported") + } + }) + return { success: undefined, error: cause.error, defect: cause.defect } + } + return { success: undefined, error: undefined, defect: undefined } + }) } const runEffectCachedFn = cache( @@ -35,7 +68,7 @@ const runEffectCachedFn = cache( effect: (...args: Args) => Effect.Effect>, ...args: Args ) => { - let promise: Promise | undefined + let promise: Promise> return (context: Context.Context>) => { if (!promise) { promise = runEffectFn(effect, context, ...args) @@ -64,10 +97,13 @@ export const reactCache = >( ): Effect.Effect> => Effect.gen(function*() { const context = yield* Effect.context>() - const value: A = yield* Effect.tryPromise({ - try: () => runEffectCachedFn(effect, ...args)(context), - catch: (e) => e as E - }) - return value + const result = yield* Effect.promise(() => runEffectCachedFn(effect, ...args)(context)) + if (result.success) { + return yield* Effect.succeed(result.success) + } + if (result.error) { + return yield* Effect.fail(result.error) + } + return yield* Effect.die(result.defect) }) }