Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,8 +971,9 @@ export const promise: <A>(
export const tryPromise: <A, E = Cause.UnknownError>(
options:
| { readonly try: (signal: AbortSignal) => PromiseLike<A>; readonly catch: (error: unknown) => E }
| { readonly try: (signal: AbortSignal) => PromiseLike<A> }
| ((signal: AbortSignal) => PromiseLike<A>)
) => Effect<A, E> = internal.tryPromise
) => Effect<A, E> = internal.promise

/**
* Creates an `Effect` that always succeeds with a given value.
Expand Down
98 changes: 60 additions & 38 deletions packages/effect/src/internal/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as Duration from "../Duration.ts"
import type * as Effect from "../Effect.ts"
import type * as Exit from "../Exit.ts"
import type * as Fiber from "../Fiber.ts"
import type { LazyArg } from "../Function.ts"
import type { FunctionN, LazyArg } from "../Function.ts"
import { constant, constFalse, constTrue, constVoid, dual, identity } from "../Function.ts"
import * as Equal from "../interfaces/Equal.ts"
import * as Hash from "../interfaces/Hash.ts"
Expand Down Expand Up @@ -890,52 +890,74 @@ const void_: Effect.Effect<void> = succeed(void 0)
export { void_ as void }

/** @internal */
const try_ = <A, E>(options: {
try: LazyArg<A>
catch: (error: unknown) => E
}): Effect.Effect<A, E> =>
suspend(() => {
function attempt<A, E = never>(
thunk: LazyArg<A>,
rescue?: ((error: unknown) => E) | undefined
): A | E {
if (rescue) {
try {
return succeed(internalCall(options.try))
} catch (err) {
return fail(internalCall(() => options.catch(err)))
return thunk()
} catch (error) {
return rescue(error)
}
})
/** @internal */
export { try_ as try }
}
return thunk()
}

type ExecOptions<Thunk, Rescue> =
| { readonly try: Thunk; readonly catch: Rescue }
| { readonly try: Thunk }
| Thunk

/** @internal */
export const promise = <A>(
evaluate: (signal: AbortSignal) => PromiseLike<A>
): Effect.Effect<A> =>
callbackOptions<A>(function(resume, signal) {
internalCall(() => evaluate(signal!)).then(
(a) => resume(succeed(a)),
(e) => resume(die(e))
export const try_ = <A, E = Cause.UnknownError>(
options: ExecOptions<LazyArg<A>, FunctionN<[error: unknown], E>>
): Effect.Effect<A, E> => {
const thunk = typeof options === "function" ? options : options.try

const recover = typeof options === "function"
? undefined
: "catch" in options
? options.catch
: (e: unknown) => new UnknownError(e, "An error occurred in Effect.sync") as E

const rescue = recover && ((e: unknown) => fail(internalCall(() => recover(e))))

return suspend(() =>
attempt(
() => succeed(internalCall(thunk)),
rescue
)
}, evaluate.length !== 0)
)
}

/** @internal */
export { try_ as try }

/** @internal */
export const tryPromise = <A, E = Cause.UnknownError>(
options: {
readonly try: (signal: AbortSignal) => PromiseLike<A>
readonly catch: (error: unknown) => E
} | ((signal: AbortSignal) => PromiseLike<A>)
export const promise = <A, E = Cause.UnknownError>(
options: ExecOptions<FunctionN<[signal: AbortSignal], PromiseLike<A>>, FunctionN<[error: unknown], E>>
): Effect.Effect<A, E> => {
const f = typeof options === "function" ? options : options.try
const catcher = typeof options === "function"
? ((cause: unknown) => new UnknownError(cause, "An error occurred in Effect.tryPromise"))
: options.catch
const thunk = typeof options === "function" ? options : options.try

const recover = typeof options === "function"
? undefined
: "catch" in options
? options.catch
: (e: unknown) => new UnknownError(e, "An error occurred in Effect.promise") as E

const rescue = recover && ((e: unknown) => fail(internalCall(() => recover(e))))

return callbackOptions<A, E>(function(resume, signal) {
try {
internalCall(() => f(signal!)).then(
(a) => resume(succeed(a)),
(e) => resume(fail(internalCall(() => catcher(e)) as E))
)
} catch (err) {
resume(fail(internalCall(() => catcher(err)) as E))
}
}, eval.length !== 0)
attempt(
() =>
internalCall(() => thunk(signal!)).then(
(a) => resume(succeed(a)),
(e) => resume(rescue ? rescue(e) : die(e))
),
rescue
)
}, thunk.length !== 0)
}

/** @internal */
Expand Down
64 changes: 64 additions & 0 deletions packages/effect/test/Effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1543,4 +1543,68 @@ describe("Effect", () => {
}))
})
})

describe("error channel vs defect channel", () => {
it.effect("Effect.try (to be renamed Effect.sync) callback form puts errors in defect channel", () =>
Effect.gen(function*() {
const program = Effect.try(() => {
throw new Error("sync error")
})

const exit = yield* Effect.exit(program)

assert.isTrue(Exit.isFailure(exit))
if (exit._tag === "Failure") {
assert.isTrue(Cause.hasDie(exit.cause))
const defect = Cause.filterDefect(exit.cause)
assert.strictEqual((defect as Error).message, "sync error")
}
}))

it.effect("Effect.try (to be renamed Effect.sync) object form without catch puts errors in error channel with UnknownError", () =>
Effect.gen(function*() {
const program = Effect.try({
try: () => {
throw new Error("sync error")
}
})

const exit = yield* Effect.exit(program)

assert.isTrue(Exit.isFailure(exit))
if (exit._tag === "Failure") {
assert.isTrue(Cause.hasFail(exit.cause))
const fail = Cause.filterError(exit.cause)
assert.strictEqual((fail as Error).message, "An error occurred in Effect.sync")
}
}))

it.effect("Effect.promise callback form puts rejections in defect channel", () =>
Effect.gen(function*() {
const program = Effect.promise(() => Promise.reject(new Error("async error")))

const exit = yield* Effect.exit(program)

assert.isTrue(Exit.isFailure(exit))
if (exit._tag === "Failure") {
assert.isTrue(Cause.hasDie(exit.cause))
const defect = Cause.filterDefect(exit.cause)
assert.strictEqual((defect as Error).message, "async error")
}
}))

it.effect("Effect.promise object form without catch puts rejections in error channel with UnknownError", () =>
Effect.gen(function*() {
const program = Effect.promise({ try: () => Promise.reject(new Error("async error")) })

const exit = yield* Effect.exit(program)

assert.isTrue(Exit.isFailure(exit))
if (exit._tag === "Failure") {
assert.isTrue(Cause.hasFail(exit.cause))
const fail = Cause.filterError(exit.cause)
assert.strictEqual((fail as Error).message, "An error occurred in Effect.promise")
}
}))
})
})