diff --git a/bench/package.json b/bench/package.json index 6d1aa890e..0ce77d714 100644 --- a/bench/package.json +++ b/bench/package.json @@ -1,4 +1,5 @@ { + "name": "effection-benchmarks", "type": "module", "private": true, "dependencies": { diff --git a/docs/agents.md b/docs/agents.md index 0bfe14a26..1d71a7d7f 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -167,7 +167,8 @@ const task = yield * op; // returns a TASK (Future) and starts it - `allSettled()` accepts an array of operations and evaluates them concurrently. - It returns an array of `Result` objects in input order - (`{ ok: true, value }` or `{ ok: false, error }`). + (`{ ok: true, value }` or `{ ok: false, error }`). Failed results also expose + `cause` as an alias for the deprecated `error` field. - It never short-circuits on error — all operations run to completion. - It is analogous to `Promise.allSettled()`, but uses Effection's `Result` shape. diff --git a/docs/async-rosetta-stone.mdx b/docs/async-rosetta-stone.mdx index 44c0f2ca8..ba56c6c7a 100644 --- a/docs/async-rosetta-stone.mdx +++ b/docs/async-rosetta-stone.mdx @@ -224,7 +224,8 @@ function* main() { Wait for all operations to settle regardless of whether they succeed or fail. Unlike [`all()`][all], [`allSettled()`][allSettled] never short-circuits. Every result is represented as either `{ ok: true, value }` -or `{ ok: false, error }`. +or `{ ok: false, error }`. Failed results also expose `cause` as an alias for the +deprecated `error` field. Wait for all promises to settle with `Promise.allSettled()`: diff --git a/docs/spawn.mdx b/docs/spawn.mdx index 19c319d37..eaa5b9be7 100644 --- a/docs/spawn.mdx +++ b/docs/spawn.mdx @@ -166,7 +166,8 @@ main(function *() { If you need to wait for all operations to settle regardless of whether they succeed or fail, use [`allSettled()`][allSettled] instead. Unlike `all()`, `allSettled()` never short-circuits on error — each result is represented as -[`Result`][result]: `{ ok: true, value }` or `{ ok: false, error }`. +[`Result`][result]: `{ ok: true, value }` or `{ ok: false, error }`. Failed +results also expose `cause` as an alias for the deprecated `error` field. ``` javascript import { allSettled, main } from 'effection'; diff --git a/lib/all-settled.ts b/lib/all-settled.ts index 7bf9f4515..f5784279b 100644 --- a/lib/all-settled.ts +++ b/lib/all-settled.ts @@ -11,7 +11,8 @@ import type { Operation, Yielded } from "./types.ts"; * * Unlike {@link all}, `allSettled` never rejects — every operation * result is represented as either `{ ok: true, value }` or - * `{ ok: false, error }`. + * `{ ok: false, error }`. Failed results also expose `cause` as an alias for + * the deprecated `error` field. * * ### Example * diff --git a/lib/result.ts b/lib/result.ts index 296a833c5..00d0dc056 100644 --- a/lib/result.ts +++ b/lib/result.ts @@ -6,7 +6,8 @@ * automatically percolate. * * A successful result has the shape `{ ok: true, value }` and a failed result - * has the shape `{ ok: false, error }`. + * has the shape `{ ok: false, error }`. Failed results created by Effection also + * expose `cause` as an alias for `error`. * * @since 4.1 */ @@ -15,6 +16,11 @@ export type Result = { value: T; } | { readonly ok: false; + /** + * Read-only alias for `error`. Optional to accept existing hand-written results. + */ + readonly cause?: Error; + /** @deprecated Use `cause` instead. */ error: Error; }; @@ -42,7 +48,8 @@ export function Ok(value?: T): Result { } /** - * Construct a failed {@link Result}. + * Construct a failed {@link Result} with `cause` and `error` referring to the + * same Error. The `cause` alias is non-enumerable to preserve the v4 object shape. * * ### Example * @@ -56,12 +63,18 @@ export function Ok(value?: T): Result { * @since 4.1 */ export function Err(cause: unknown): Result { - return { + let result: Result = { ok: false, error: cause instanceof Error ? cause : new ThrownValueError(String(cause), { cause }), }; + return Object.defineProperty(result, "cause", { + configurable: true, + get() { + return result.error; + }, + }); } class ThrownValueError extends Error { diff --git a/test/all-settled.test.ts b/test/all-settled.test.ts index 1409b747a..ad1c06764 100644 --- a/test/all-settled.test.ts +++ b/test/all-settled.test.ts @@ -111,6 +111,38 @@ describe("allSettled()", () => { expect(result.length).toEqual(3); }); + it("exposes cause as an alias for error in failed results", async () => { + let causes = [ + new Error("boom"), + { message: "boom" }, + "boom", + null, + undefined, + ]; + let results = await run(() => + allSettled(causes.map((cause) => + call(function* () { + yield* sleep(0); + throw cause; + }) + )) + ); + + for (let [index, result] of results.entries()) { + if (result.ok) { + throw new Error("expected a failed result"); + } + expect(result.cause).toBe(result.error); + expect(result.cause).toBeInstanceOf(Error); + let cause = causes[index]; + if (cause instanceof Error) { + expect(result.cause).toBe(cause); + } else { + expect(result.cause?.cause).toBe(cause); + } + } + }); + it("does not halt sibling operations when one fails", async () => { let teardown = false; await run(function* () { diff --git a/test/result.test.ts b/test/result.test.ts index 4a9b3bb5d..7f48ad070 100644 --- a/test/result.test.ts +++ b/test/result.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it } from "./suite.ts"; +import { describe, expect, expectType, it } from "./suite.ts"; -import { Err, Ok, unbox } from "../mod.ts"; +import { Err, Ok, type Result, unbox } from "../mod.ts"; describe("Result", () => { it("constructs successful results with Ok()", () => { @@ -24,6 +24,43 @@ describe("Result", () => { expect(result.error.name).toEqual("ThrownValueError"); expect(result.error.message).toEqual("oh no"); expect(result.error.cause).toEqual("oh no"); + expect(result.cause).toBe(result.error); + }); + + it("aliases cause to error without changing the v4 object shape", () => { + let error = new Error("oh no"); + let result = Err(error); + + if (result.ok) { + throw new Error("expected a failed result"); + } + + expect(result.cause).toBe(error); + expect({ ...result }).toEqual({ ok: false, error }); + expectType(result.error); + expectType(result.cause); + }); + + it("reflects updates to error through cause", () => { + let result = Err(new Error("original")); + + if (result.ok) { + throw new Error("expected a failed result"); + } + + let error = new Error("changed via error"); + result.error = error; + expect(result.cause).toBe(error); + + expect(() => unbox(result)).toThrow(error); + }); + + it("accepts existing hand-written results without cause", () => { + let error = new Error("oh no"); + let result: Result = { ok: false, error }; + + expect(result.cause).toBeUndefined(); + expect(() => unbox(result)).toThrow(error); }); it("unboxes non-Error causes", () => {