diff --git a/lib/action.ts b/lib/action.ts index 2ee27fb28..05a09fd6d 100644 --- a/lib/action.ts +++ b/lib/action.ts @@ -63,7 +63,7 @@ export function action(executor: Executor, desc?: string): Operation { discard(); discarded(Ok()); } catch (error) { - discarded(Err(error as Error)); + discarded(Err(error)); } }; }, diff --git a/lib/box.ts b/lib/box.ts index ac14f01de..202135ae7 100644 --- a/lib/box.ts +++ b/lib/box.ts @@ -5,6 +5,6 @@ export function* box(op: () => Operation): Operation> { try { return Ok(yield* op()); } catch (error) { - return Err(error as Error); + return Err(error); } } diff --git a/lib/delimiter.ts b/lib/delimiter.ts index a262961e8..e16db182f 100644 --- a/lib/delimiter.ts +++ b/lib/delimiter.ts @@ -86,7 +86,7 @@ export class Delimiter } } catch (error) { this.computed = true; - this.outcome = Just(Err(error as Error)); + this.outcome = Just(Err(error)); } finally { this.finalized = true; this.outcome = this.outcome ?? Nothing(); diff --git a/lib/race.ts b/lib/race.ts index fe60f4b80..f64312603 100644 --- a/lib/race.ts +++ b/lib/race.ts @@ -45,7 +45,7 @@ export function* race>( let value = yield* operation; winner.resolve(Ok(value as Yielded)); } catch (error) { - winner.resolve(Err(error as Error)); + winner.resolve(Err(error)); } }), ); diff --git a/lib/reducer.ts b/lib/reducer.ts index 45822a468..919ab5ec5 100644 --- a/lib/reducer.ts +++ b/lib/reducer.ts @@ -48,7 +48,7 @@ export class Reducer { throw result.error; } } catch (error) { - routine.next(Err(error as Error)); + routine.next(Err(error)); } item = queue.dequeue(); } diff --git a/lib/result.ts b/lib/result.ts index 8649a2468..21b54ec14 100644 --- a/lib/result.ts +++ b/lib/result.ts @@ -1,5 +1,14 @@ /** - * @ignore + * A value representing either a successful outcome or an error. + * + * `Result` is used in APIs when you want to make explicit flow control + * decisions about success/failure rather than allowing them to + * automatically percolate. + * + * A successful result has the shape `{ ok: true, value }` and a failed result + * has the shape `{ ok: false, error }`. + * + * @since 4.1 */ export type Result = { readonly ok: true; @@ -10,7 +19,18 @@ export type Result = { }; /** - * @ignore + * Construct a successful {@link Result}. + * + * ### Example + * + * ```javascript + * import { Ok } from 'effection'; + * + * let result = Ok("hello"); + * // { ok: true, value: "hello" } + * ``` + * + * @since 4.1 */ export function Ok(): Result; export function Ok(value: T): Result; @@ -22,9 +42,34 @@ export function Ok(value?: T): Result { } /** - * @ignore + * Construct a failed {@link Result}. + * + * ### Example + * + * ```javascript + * import { Err } from 'effection'; + * + * let result = Err(new Error("oh no")); + * // { ok: false, error: Error("oh no") } + * ``` + * + * @since 4.1 */ -export const Err = (error: Error): Result => ({ ok: false, error }); +export function Err(cause: unknown): Result { + return { + ok: false, + error: cause instanceof Error + ? cause + : new ThrownValueError(String(cause), { cause }), + }; +} + +class ThrownValueError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "ThrownValueError"; + } +} /** * @ignore diff --git a/lib/scope-internal.ts b/lib/scope-internal.ts index 0e505f4f6..326ec0451 100644 --- a/lib/scope-internal.ts +++ b/lib/scope-internal.ts @@ -79,7 +79,7 @@ export function createScopeInternal( destructors.delete(destructor); yield* destructor(); } catch (error) { - outcome = Err(error as Error); + outcome = Err(error); } } } finally { diff --git a/test/result.test.ts b/test/result.test.ts new file mode 100644 index 000000000..99f31fddb --- /dev/null +++ b/test/result.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "./suite.ts"; + +import { Err, Ok } from "../mod.ts"; + +describe("Result", () => { + it("constructs successful results with Ok()", () => { + expect(Ok("hello")).toEqual({ ok: true, value: "hello" }); + }); + + it("preserves Error instances passed to Err()", () => { + let error = new Error("oh no"); + + expect(Err(error)).toEqual({ ok: false, error }); + }); + + it("wraps non-Error causes passed to Err()", () => { + let result = Err("oh no"); + + if (result.ok) { + throw new Error("expected Err() to produce a failed result"); + } + + expect(result.error).toBeInstanceOf(Error); + expect(result.error.name).toEqual("ThrownValueError"); + expect(result.error.message).toEqual("oh no"); + expect(result.error.cause).toEqual("oh no"); + }); +}); diff --git a/test/until.test.ts b/test/until.test.ts index a15ca61a1..89b4b8057 100644 --- a/test/until.test.ts +++ b/test/until.test.ts @@ -9,13 +9,15 @@ describe("until", () => { expect(yield* until(Promise.resolve(42))).toEqual(42); }); }); - it("throws on error", async () => { - expect.assertions(1); + it("wraps non-Error promise rejections", async () => { + expect.assertions(3); await run(function* () { try { yield* until(Promise.reject("error")); } catch (error) { - expect(error).toBe("error"); + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toBe("error"); + expect((error as Error).cause).toBe("error"); } }); });