From 95021b2009d4061557fb1f3cd065c84316f723c2 Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Sun, 1 Nov 2026 18:38:18 -0800 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20add=20Result.reason=20alias=20a?= =?UTF-8?q?nd=20deprecate=20error=20in=20v4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/all-settled.ts | 3 ++- lib/result.ts | 19 ++++++++++++++++--- test/all-settled.test.ts | 32 +++++++++++++++++++++++++++++++ test/result.test.ts | 41 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/lib/all-settled.ts b/lib/all-settled.ts index 7bf9f4515..533db7c5a 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 `reason` as an alias for + * the deprecated `error` field. * * ### Example * diff --git a/lib/result.ts b/lib/result.ts index 296a833c5..6f7674c23 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 `reason` 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 reason?: Error; + /** @deprecated Use `reason` instead. */ error: Error; }; @@ -42,7 +48,8 @@ export function Ok(value?: T): Result { } /** - * Construct a failed {@link Result}. + * Construct a failed {@link Result} with `reason` and `error` referring to the + * same Error. The `reason` 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, "reason", { + 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..53e548604 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 reason 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.reason).toBe(result.error); + expect(result.reason).toBeInstanceOf(Error); + let cause = causes[index]; + if (cause instanceof Error) { + expect(result.reason).toBe(cause); + } else { + expect(result.reason?.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..bfb785b71 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.reason).toBe(result.error); + }); + + it("aliases reason 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.reason).toBe(error); + expect({ ...result }).toEqual({ ok: false, error }); + expectType(result.error); + expectType(result.reason); + }); + + it("reflects updates to error through reason", () => { + 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.reason).toBe(error); + + expect(() => unbox(result)).toThrow(error); + }); + + it("accepts existing hand-written results without reason", () => { + let error = new Error("oh no"); + let result: Result = { ok: false, error }; + + expect(result.reason).toBeUndefined(); + expect(() => unbox(result)).toThrow(error); }); it("unboxes non-Error causes", () => { From dd2f582d0332504db006b72aca0b285e2bdafa54 Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Sun, 1 Nov 2026 18:38:18 -0800 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=A4=96=20document=20the=20v4=20Result?= =?UTF-8?q?.reason=20alias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/agents.md | 3 ++- docs/async-rosetta-stone.mdx | 3 ++- docs/spawn.mdx | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/agents.md b/docs/agents.md index 0bfe14a26..450b97fa4 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 + `reason` 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..933cf0b9a 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 `reason` 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..34e1da3c9 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 `reason` as an alias for the deprecated `error` field. ``` javascript import { allSettled, main } from 'effection'; From 10671e13f28cb1ca736d29293fa58a2fc25374f0 Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Sun, 1 Nov 2026 18:54:32 -0800 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=92=9A=20fix=20benchmark=20installati?= =?UTF-8?q?on=20and=20race=20test=20timing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bench/package.json | 1 + test/race.test.ts | 29 +++++++++++++++++------------ 2 files changed, 18 insertions(+), 12 deletions(-) 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/test/race.test.ts b/test/race.test.ts index a5628fcae..8965ea44c 100644 --- a/test/race.test.ts +++ b/test/race.test.ts @@ -1,6 +1,4 @@ import { - asyncReject, - asyncResolve, describe, expect, expectType, @@ -17,30 +15,37 @@ import { sleep, spawn, suspend, + withResolvers, } from "../mod.ts"; describe("race()", () => { it("resolves when one of the given operations resolves asynchronously first", async () => { + let first = withResolvers(); + let second = withResolvers(); + let third = withResolvers(); let result = run(() => - race([ - asyncResolve(10, "foo"), - asyncResolve(5, "bar"), - asyncReject(15, "baz"), - ]) + race([first.operation, second.operation, third.operation]) ); + second.resolve("bar"); + first.resolve("foo"); + third.reject(new Error("boom: baz")); + await expect(result).resolves.toEqual("bar"); }); it("rejects when one of the given operations rejects asynchronously first", async () => { + let first = withResolvers(); + let second = withResolvers(); + let third = withResolvers(); let result = run(() => - race([ - asyncResolve(10, "foo"), - asyncReject(5, "bar"), - asyncReject(15, "baz"), - ]) + race([first.operation, second.operation, third.operation]) ); + second.reject(new Error("boom: bar")); + first.resolve("foo"); + third.reject(new Error("boom: baz")); + await expect(result).rejects.toHaveProperty("message", "boom: bar"); }); From 5b2e8c3bced7bc4c49e0c0831b1b6768717bf0f3 Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Sun, 1 Nov 2026 19:09:21 -0800 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20name=20the=20v4=20Resu?= =?UTF-8?q?lt=20alias=20cause?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/all-settled.ts | 2 +- lib/result.ts | 12 ++++++------ test/all-settled.test.ts | 10 +++++----- test/result.test.ts | 16 ++++++++-------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/all-settled.ts b/lib/all-settled.ts index 533db7c5a..f5784279b 100644 --- a/lib/all-settled.ts +++ b/lib/all-settled.ts @@ -11,7 +11,7 @@ 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 }`. Failed results also expose `reason` as an alias for + * `{ 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 6f7674c23..00d0dc056 100644 --- a/lib/result.ts +++ b/lib/result.ts @@ -7,7 +7,7 @@ * * A successful result has the shape `{ ok: true, value }` and a failed result * has the shape `{ ok: false, error }`. Failed results created by Effection also - * expose `reason` as an alias for `error`. + * expose `cause` as an alias for `error`. * * @since 4.1 */ @@ -19,8 +19,8 @@ export type Result = { /** * Read-only alias for `error`. Optional to accept existing hand-written results. */ - readonly reason?: Error; - /** @deprecated Use `reason` instead. */ + readonly cause?: Error; + /** @deprecated Use `cause` instead. */ error: Error; }; @@ -48,8 +48,8 @@ export function Ok(value?: T): Result { } /** - * Construct a failed {@link Result} with `reason` and `error` referring to the - * same Error. The `reason` alias is non-enumerable to preserve the v4 object shape. + * 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 * @@ -69,7 +69,7 @@ export function Err(cause: unknown): Result { ? cause : new ThrownValueError(String(cause), { cause }), }; - return Object.defineProperty(result, "reason", { + return Object.defineProperty(result, "cause", { configurable: true, get() { return result.error; diff --git a/test/all-settled.test.ts b/test/all-settled.test.ts index 53e548604..ad1c06764 100644 --- a/test/all-settled.test.ts +++ b/test/all-settled.test.ts @@ -111,7 +111,7 @@ describe("allSettled()", () => { expect(result.length).toEqual(3); }); - it("exposes reason as an alias for error in failed results", async () => { + it("exposes cause as an alias for error in failed results", async () => { let causes = [ new Error("boom"), { message: "boom" }, @@ -132,13 +132,13 @@ describe("allSettled()", () => { if (result.ok) { throw new Error("expected a failed result"); } - expect(result.reason).toBe(result.error); - expect(result.reason).toBeInstanceOf(Error); + expect(result.cause).toBe(result.error); + expect(result.cause).toBeInstanceOf(Error); let cause = causes[index]; if (cause instanceof Error) { - expect(result.reason).toBe(cause); + expect(result.cause).toBe(cause); } else { - expect(result.reason?.cause).toBe(cause); + expect(result.cause?.cause).toBe(cause); } } }); diff --git a/test/result.test.ts b/test/result.test.ts index bfb785b71..7f48ad070 100644 --- a/test/result.test.ts +++ b/test/result.test.ts @@ -24,10 +24,10 @@ describe("Result", () => { expect(result.error.name).toEqual("ThrownValueError"); expect(result.error.message).toEqual("oh no"); expect(result.error.cause).toEqual("oh no"); - expect(result.reason).toBe(result.error); + expect(result.cause).toBe(result.error); }); - it("aliases reason to error without changing the v4 object shape", () => { + it("aliases cause to error without changing the v4 object shape", () => { let error = new Error("oh no"); let result = Err(error); @@ -35,13 +35,13 @@ describe("Result", () => { throw new Error("expected a failed result"); } - expect(result.reason).toBe(error); + expect(result.cause).toBe(error); expect({ ...result }).toEqual({ ok: false, error }); expectType(result.error); - expectType(result.reason); + expectType(result.cause); }); - it("reflects updates to error through reason", () => { + it("reflects updates to error through cause", () => { let result = Err(new Error("original")); if (result.ok) { @@ -50,16 +50,16 @@ describe("Result", () => { let error = new Error("changed via error"); result.error = error; - expect(result.reason).toBe(error); + expect(result.cause).toBe(error); expect(() => unbox(result)).toThrow(error); }); - it("accepts existing hand-written results without reason", () => { + it("accepts existing hand-written results without cause", () => { let error = new Error("oh no"); let result: Result = { ok: false, error }; - expect(result.reason).toBeUndefined(); + expect(result.cause).toBeUndefined(); expect(() => unbox(result)).toThrow(error); }); From 5b3d6676504771c5c4e44fe27cfb80acf818195f Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Sun, 1 Nov 2026 19:09:22 -0800 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=A4=96=20document=20the=20v4=20Result?= =?UTF-8?q?.cause=20alias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/agents.md | 2 +- docs/async-rosetta-stone.mdx | 2 +- docs/spawn.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/agents.md b/docs/agents.md index 450b97fa4..1d71a7d7f 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -168,7 +168,7 @@ 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 }`). Failed results also expose - `reason` as an alias for the deprecated `error` field. + `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 933cf0b9a..ba56c6c7a 100644 --- a/docs/async-rosetta-stone.mdx +++ b/docs/async-rosetta-stone.mdx @@ -224,7 +224,7 @@ 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 }`. Failed results also expose `reason` as an alias for the +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 34e1da3c9..eaa5b9be7 100644 --- a/docs/spawn.mdx +++ b/docs/spawn.mdx @@ -167,7 +167,7 @@ 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 }`. Failed -results also expose `reason` as an alias for the deprecated `error` field. +results also expose `cause` as an alias for the deprecated `error` field. ``` javascript import { allSettled, main } from 'effection'; From 1e7b52edbc8d3915a2e4384d3a2d569736ca958c Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Mon, 2 Nov 2026 13:24:21 -0800 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=85=20restore=20asynchronous=20race?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/race.test.ts | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/test/race.test.ts b/test/race.test.ts index 8965ea44c..a5628fcae 100644 --- a/test/race.test.ts +++ b/test/race.test.ts @@ -1,4 +1,6 @@ import { + asyncReject, + asyncResolve, describe, expect, expectType, @@ -15,37 +17,30 @@ import { sleep, spawn, suspend, - withResolvers, } from "../mod.ts"; describe("race()", () => { it("resolves when one of the given operations resolves asynchronously first", async () => { - let first = withResolvers(); - let second = withResolvers(); - let third = withResolvers(); let result = run(() => - race([first.operation, second.operation, third.operation]) + race([ + asyncResolve(10, "foo"), + asyncResolve(5, "bar"), + asyncReject(15, "baz"), + ]) ); - second.resolve("bar"); - first.resolve("foo"); - third.reject(new Error("boom: baz")); - await expect(result).resolves.toEqual("bar"); }); it("rejects when one of the given operations rejects asynchronously first", async () => { - let first = withResolvers(); - let second = withResolvers(); - let third = withResolvers(); let result = run(() => - race([first.operation, second.operation, third.operation]) + race([ + asyncResolve(10, "foo"), + asyncReject(5, "bar"), + asyncReject(15, "baz"), + ]) ); - second.reject(new Error("boom: bar")); - first.resolve("foo"); - third.reject(new Error("boom: baz")); - await expect(result).rejects.toHaveProperty("message", "boom: bar"); });