Skip to content

fix: support frozen intrinsics when adjusting Error.stackTraceLimit#6279

Open
bweis wants to merge 2 commits into
Effect-TS:mainfrom
bweis:fix/frozen-intrinsics-stack-trace-limit
Open

fix: support frozen intrinsics when adjusting Error.stackTraceLimit#6279
bweis wants to merge 2 commits into
Effect-TS:mainfrom
bweis:fix/frozen-intrinsics-stack-trace-limit

Conversation

@bweis

@bweis bweis commented Jun 17, 2026

Copy link
Copy Markdown

In hardened/deterministic JavaScript environments (SES "frozen intrinsics", Temporal-style sandboxes), Error is frozen and Error.stackTraceLimit is read-only. Effect mutates stackTraceLimit in several internal spots to capture short/empty stack traces cheaply; under frozen intrinsics those assignments throw, breaking Effect entirely.

Add an internal stackTraceLimit helper (mirroring Node's own guard) that detects writability once and degrades set to a silent no-op when the property can't be modified. Replace the raw mutations across Effect.ts, Micro.ts, LayerMap.ts, and the internal context/core-effect/layer/runtime/ tracer/cause modules with guarded get/set calls, keeping new Error() inline at each call site so captured stack traces still point at the real caller.

@effect/ai's unsafeSecureJsonParse gets an equivalent inline guard (no wrapping closure) since the effect helper is not part of the public API.

Type

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Description

Effect manipulates Error.stackTraceLimit in several internal spots (Tag/Service/fn creation, tracer spans, fiber failures, pretty errors, etc.) to capture short or empty stack traces cheaply, using the pattern:

const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const err = new Error()
Error.stackTraceLimit = limit

In environments with frozen intrinsics (SES / hardened JavaScript, deterministic sandboxes such as Temporal), Error.stackTraceLimit is non-writable and these assignments throw, breaking Effect entirely.

This PR introduces packages/effect/src/internal/stackTraceLimit.ts, which checks writability once at module load (mirroring Node's internal guard) and exposes getStackTraceLimit() / setStackTraceLimit() helpers where set is a silent no-op when the property can't be modified. All raw mutations are replaced with these guarded calls, with new Error() kept inline at each call site so the captured stack trace still points at the real caller rather than the helper.

@effect/ai's unsafeSecureJsonParse gets an equivalent inline guard because the effect helper is @internal and not part of the public API. Note that this call site uses a plain if (!canWriteStackTraceLimit) { ... } branch around the mutation rather than a withStackTraceLimit(limit, fn) wrapper: running the body inside a closure would push an extra frame onto any captured stack trace and change its shape (per review feedback). With the inline guard, the writable path is byte-identical to the original.

Behavior in normal (writable) environments is unchanged. A changeset and a dedicated test (packages/effect/test/StackTraceLimit.test.ts, covering both the writable and frozen paths) are included; the existing test/Effect, Cause, Tracer, Context, LayerMap, Micro, and @effect/ai Tool suites pass.

Note

The original PR for this change was stood up by GitHub user @arlyon. This description and PR were aided via Claude Code.

In hardened/deterministic JavaScript environments (SES "frozen intrinsics",
Temporal-style sandboxes), `Error` is frozen and `Error.stackTraceLimit` is
read-only. Effect mutates `stackTraceLimit` in several internal spots to
capture short/empty stack traces cheaply; under frozen intrinsics those
assignments throw, breaking Effect entirely.

Add an internal `stackTraceLimit` helper (mirroring Node's own guard) that
detects writability once and degrades `set` to a silent no-op when the
property can't be modified. Replace the raw mutations across Effect.ts,
Micro.ts, LayerMap.ts, and the internal context/core-effect/layer/runtime/
tracer/cause modules with guarded get/set calls, keeping `new Error()` inline
at each call site so captured stack traces still point at the real caller.

`@effect/ai`'s `unsafeSecureJsonParse` gets an equivalent inline snippet since
the effect helper is not part of the public API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-project-automation github-project-automation Bot moved this to Discussion Ongoing in PR Backlog Jun 17, 2026
@changeset-bot

changeset-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f15507b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
effect Patch
@effect/ai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@bweis bweis mentioned this pull request Jun 17, 2026
5 tasks
@bweis

bweis commented Jun 18, 2026

Copy link
Copy Markdown
Author

Per discussion in Discord: @tim-smart mind taking a look at this?

Comment thread packages/ai/ai/src/Tool.ts Outdated

const canWriteStackTraceLimit = isStackTraceLimitWritable()

const withStackTraceLimit = <T>(limit: number, fn: () => T): T => {

@arlyon arlyon Jun 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withStackTraceLimit will introduce a stack frame so it changes the shape of the trace

Comment from @mikearnaldi on the original PR since this was also my direction

#5868 (comment)

@arlyon arlyon Jun 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The worm took my post-review changes and force pushed / rewrote them. See here for the clean post-review commit from 6mo ago

1757334

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack. Great point. Now fixed in the latest commit.

The withStackTraceLimit(limit, fn) closure helper ran _parse inside a
callback, pushing an extra frame onto any captured stack trace and
changing its shape (flagged in review). Replace it with an inline guard
so there is no wrapping closure; the writable path is now byte-identical
to the original, and the frozen path simply skips the mutation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bweis bweis requested a review from arlyon June 19, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Discussion Ongoing

Development

Successfully merging this pull request may close these issues.

2 participants