Skip to content

🐛 preserve unknown error causes - #1229

Open
jrandolf wants to merge 1 commit into
thefrontside:v5from
jrandolf:jrandolf/error-unknown
Open

jrandolf wants to merge 1 commit into
thefrontside:v5from
jrandolf:jrandolf/error-unknown

Conversation

@jrandolf

@jrandolf jrandolf commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Effection v4 preserves thrown values during automatic propagation (#1230), but explicit Err() and allSettled() results still wrap non-Error values. This v5 change preserves their identity in explicit results too, as agreed in the discussion.

Approach

Make Result.error and rejection parameters unknown, store causes unchanged, and remove ThrownValueError. Retain the v4 propagation tests and add coverage for explicit results and repeated resolver consumption.

@pkg-pr-new

pkg-pr-new Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/effection@1229

commit: f4ceb1b

@codspeed

codspeed Bot commented Aug 27, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 5.04%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 4 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Memory effection-inline.recursion 4.7 KB 5.8 KB -19.35%
⚡ Memory effection.events 98.5 KB 88.1 KB +11.82%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing jrandolf:jrandolf/error-unknown (a00070f) with v4 (70ddc1c)

Open in CodSpeed

@cowboyd

cowboyd commented Aug 27, 2026

Copy link
Copy Markdown
Member

Hey @jrandolf, Thanks for the PR!

This is definitely a breaking change. I'm not opposed to it, but I'm curious why you think it is lossy? All information is retained, and If you are capturing a result object in the first place then by definition you are already preventing code upwards on the stack from seeing the throw value. In order to re-expose the stack to the error, you are would already need to destructure via the .ok guard. It seems to me at the point of de-structuring, you could re-raise any thrown value at the same point.

export function unbox<T>(result: Result<T>): T {
  if (result.ok) {
    return result.value;
  } else {
    throw result.error instanceof ThrowValueError ? result.error.cause : result.error;
  }
}

Or is the problem that Effection itself does not do this when it re-throws an error? That would definitely be a problem

@jrandolf

jrandolf commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Yes. Effection itself rethrows the wrapper.

#1149 established Result.error: Error, added ThrownValueError, and added coverage requiring until(Promise.reject("error")) to expose that wrapper. The behavior is deliberate.

Changing unbox() would fix the paths that call it, including callcc, scope destruction, and task-group shutdown. Other runtime paths bypass it: Do, race, coroutine resumption, the task iterator, the task Promise adapter, and background error propagation all use the normalized result.error directly. Preserving Result.error: Error while restoring the original thrown value would require the same unwrapping rule at each of those boundaries.

This PR changes the common representation instead. Result.error becomes unknown, and Err() stores the exact cause, so explicit results and automatic propagation follow the same rule. The wrapper retains the original information in cause; the behavioral difference is that Effection changes the identity and observable type of the caught value.

SvelteKit shows why that matters. Its HttpError and Redirect are plain classes, and isHttpError() and isRedirect() recognize them with instanceof. Wrapping either value breaks those guards. Next.js does not have this problem because its redirect sentinel is an Error and already passes through unchanged.

@cowboyd

cowboyd commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Right. This could be a change that could be accepted as part of a 5.0 release, but since it breaks a public API, it cannot go into the v4 release series as-is. However, failure to unbox is a bug in v4 and should be fixed.

@cowboyd

cowboyd commented Aug 30, 2026

Copy link
Copy Markdown
Member

I think the path forward here is to start a v5 branch with this as the first change on it, and then add the unwrapping behavior to v4. Is that latter item something you'd be willing to take on?

@jrandolf

Copy link
Copy Markdown
Contributor Author

Let's do it!

@jrandolf

Copy link
Copy Markdown
Contributor Author

@cowboyd I opened #1230 with the v4-compatible fix: explicit Result.error values remain Error, while automatic operation, coroutine, task, race, halt, and Promise boundaries restore the original thrown value. The full 235-step suite, lint, formatting, and type checks pass. I left this PR unchanged for the future v5 branch.

@jrandolf

Copy link
Copy Markdown
Contributor Author

@cowboyd Will you create the v5 branch? I don't have permission for this.

@cowboyd

cowboyd commented Oct 31, 2026

Copy link
Copy Markdown
Member

@jrandolf done!

@jrandolf

Copy link
Copy Markdown
Contributor Author

@cowboyd This PR is ready!

This branch was successfully deployed

1 active deployment
Preview — f4ceb1b3 Deployed Oct 31, 2026 by jrandolf via deploy-preview #1438
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants