Skip to content

Commit c1dfd60

Browse files
bweisclaude
andauthored
fix: support frozen intrinsics when adjusting Error.stackTraceLimit (#2444)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 074e436 commit c1dfd60

11 files changed

Lines changed: 169 additions & 35 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Avoid throwing when `Error.stackTraceLimit` is non-writable (frozen intrinsics / SES / deterministic sandboxes such as Temporal).
6+
7+
Effect manipulates `Error.stackTraceLimit` in several internal spots to capture short or empty stack traces cheaply. In hardened environments where `Error` is frozen and `stackTraceLimit` is read-only, assigning to it throws, which broke Effect entirely. Stack-trace-limit manipulation is now best-effort and silently no-ops when the property cannot be modified, mirroring Node's own internal guard. Behavior in normal (writable) environments is unchanged.

packages/effect/src/Context.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { dual, type LazyArg } from "./Function.ts"
1717
import * as Hash from "./Hash.ts"
1818
import type { Inspectable } from "./Inspectable.ts"
1919
import { exitSucceed, PipeInspectableProto, withFiber } from "./internal/core.ts"
20-
import type { ErrorWithStackTraceLimit } from "./internal/tracer.ts"
20+
import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts"
2121
import * as Option from "./Option.ts"
2222
import type { Pipeable } from "./Pipeable.ts"
2323
import { hasProperty } from "./Predicate.ts"
@@ -231,11 +231,10 @@ export const Service: {
231231
>
232232
& { readonly make: Make }
233233
} = function() {
234-
const prevLimit = (Error as ErrorWithStackTraceLimit).stackTraceLimit
235-
;(Error as ErrorWithStackTraceLimit)
236-
.stackTraceLimit = 2
234+
const prevLimit = getStackTraceLimit()
235+
setStackTraceLimit(2)
237236
const err = new Error()
238-
;(Error as ErrorWithStackTraceLimit).stackTraceLimit = prevLimit
237+
setStackTraceLimit(prevLimit)
239238
function KeyClass() {}
240239
const self = KeyClass as any as Types.Mutable<Reference<any>>
241240
Object.setPrototypeOf(self, ServiceProto)

packages/effect/src/Layer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type { LazyArg } from "./Function.ts"
2020
import { constant, constTrue, constUndefined, dual, identity } from "./Function.ts"
2121
import * as core from "./internal/core.ts"
2222
import * as internalEffect from "./internal/effect.ts"
23-
import type { ErrorWithStackTraceLimit } from "./internal/tracer.ts"
23+
import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts"
2424
import * as internalTracer from "./internal/tracer.ts"
2525
import { type Pipeable, pipeArguments } from "./Pipeable.ts"
2626
import { hasProperty } from "./Predicate.ts"
@@ -2276,10 +2276,10 @@ const mockImpl = <I, S extends object>(service: Context.Key<I, S>, implementatio
22762276
if (prop in target) {
22772277
return target[prop as keyof S]
22782278
}
2279-
const prevLimit = (Error as ErrorWithStackTraceLimit).stackTraceLimit
2280-
;(Error as ErrorWithStackTraceLimit).stackTraceLimit = 2
2279+
const prevLimit = getStackTraceLimit()
2280+
setStackTraceLimit(2)
22812281
const error = new Error(`${service.key}: Unimplemented method "${prop.toString()}"`)
2282-
;(Error as ErrorWithStackTraceLimit).stackTraceLimit = prevLimit
2282+
setStackTraceLimit(prevLimit)
22832283
error.name = "UnimplementedError"
22842284
return makeUnimplemented(error)
22852285
},

packages/effect/src/LayerMap.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as Context from "./Context.ts"
1313
import type * as Duration from "./Duration.ts"
1414
import * as Effect from "./Effect.ts"
1515
import { identity } from "./Function.ts"
16+
import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts"
1617
import * as Layer from "./Layer.ts"
1718
import * as RcMap from "./RcMap.ts"
1819
import * as Scope from "./Scope.ts"
@@ -383,10 +384,10 @@ export const Service = <Self>() =>
383384
: never
384385
> => {
385386
const Err = globalThis.Error as any
386-
const limit = Err.stackTraceLimit
387-
Err.stackTraceLimit = 2
387+
const limit = getStackTraceLimit()
388+
setStackTraceLimit(2)
388389
const creationError = new Err()
389-
Err.stackTraceLimit = limit
390+
setStackTraceLimit(limit)
390391

391392
function TagClass() {}
392393
const TagClass_ = TagClass as any as Mutable<TagClass<Self, Id, string, any, any, any, any, any>>

packages/effect/src/internal/effect.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ import {
9696
TracerSpanLinks,
9797
TracerTimingEnabled
9898
} from "./references.ts"
99-
import { addSpanStackTrace, type ErrorWithStackTraceLimit, makeStackCleaner } from "./tracer.ts"
99+
import { getStackTraceLimit, setStackTraceLimit } from "./stackTraceLimit.ts"
100+
import { addSpanStackTrace, makeStackCleaner } from "./tracer.ts"
100101
import { version } from "./version.ts"
101102

102103
// ----------------------------------------------------------------------------
@@ -314,9 +315,8 @@ export const causePrettyErrors = <E>(self: Cause.Cause<E>): Array<Error> => {
314315
const interrupts: Array<Cause.Interrupt> = []
315316
if (self.reasons.length === 0) return errors
316317

317-
const prevStackLimit = (Error as ErrorWithStackTraceLimit).stackTraceLimit
318-
;(Error as ErrorWithStackTraceLimit)
319-
.stackTraceLimit = 1
318+
const prevStackLimit = getStackTraceLimit()
319+
setStackTraceLimit(1)
320320

321321
for (const failure of self.reasons) {
322322
if (failure._tag === "Interrupt") {
@@ -340,7 +340,7 @@ export const causePrettyErrors = <E>(self: Cause.Cause<E>): Array<Error> => {
340340
errors.push(causePrettyError(error, interrupts[0].annotations))
341341
}
342342

343-
;(Error as ErrorWithStackTraceLimit).stackTraceLimit = prevStackLimit
343+
setStackTraceLimit(prevStackLimit)
344344
return errors
345345
}
346346

@@ -1157,10 +1157,10 @@ export const fn: typeof Effect.fn = function() {
11571157
const name = nameFirst ? arguments[0] : "Effect.fn"
11581158
const spanOptions = nameFirst ? arguments[1] : undefined
11591159

1160-
const prevLimit = globalThis.Error.stackTraceLimit
1161-
globalThis.Error.stackTraceLimit = 2
1160+
const prevLimit = getStackTraceLimit()
1161+
setStackTraceLimit(2)
11621162
const defError = new globalThis.Error()
1163-
globalThis.Error.stackTraceLimit = prevLimit
1163+
setStackTraceLimit(prevLimit)
11641164

11651165
if (nameFirst) {
11661166
return (body: Function | { readonly self: any }, ...pipeables: Array<Function>) =>
@@ -1200,10 +1200,10 @@ const makeFn = (
12001200
if (!isEffect(result)) {
12011201
return result
12021202
}
1203-
const prevLimit = globalThis.Error.stackTraceLimit
1204-
globalThis.Error.stackTraceLimit = 2
1203+
const prevLimit = getStackTraceLimit()
1204+
setStackTraceLimit(2)
12051205
const callError = new globalThis.Error()
1206-
globalThis.Error.stackTraceLimit = prevLimit
1206+
setStackTraceLimit(prevLimit)
12071207
return updateService(
12081208
addSpan ?
12091209
useSpan(name, spanOptions!, (span) => provideParentSpan(result, span)) :
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Utility for safely manipulating `Error.stackTraceLimit` in environments
3+
* where intrinsics may be frozen (e.g., SES / hardened JavaScript or
4+
* deterministic sandboxes such as Temporal). When the property is non-writable,
5+
* mutating it throws, so all manipulation here degrades to a best-effort,
6+
* silent no-op.
7+
*
8+
* Mirrors the guard Node uses internally:
9+
* https://github.com/nodejs/node/blob/e77694631f1642c302f664703197b5aabc65b482/lib/internal/errors.js#L246
10+
*
11+
* The error is constructed inline at each call site (rather than inside a
12+
* closure here) so the captured stack trace keeps pointing at the real caller
13+
* instead of this module.
14+
*
15+
* @internal
16+
*/
17+
import type { ErrorWithStackTraceLimit } from "./tracer.ts"
18+
19+
const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor
20+
const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty
21+
const ObjectIsExtensible = Object.isExtensible
22+
23+
/**
24+
* Check if `Error.stackTraceLimit` is writable.
25+
* Returns `false` if the property is frozen, non-writable, or `Error` is non-extensible.
26+
*
27+
* @internal
28+
*/
29+
export const isStackTraceLimitWritable = (): boolean => {
30+
const desc = ObjectGetOwnPropertyDescriptor(Error, "stackTraceLimit")
31+
if (desc === undefined) {
32+
return ObjectIsExtensible(Error)
33+
}
34+
35+
return ObjectPrototypeHasOwnProperty.call(desc, "writable")
36+
? desc.writable === true
37+
: desc.set !== undefined
38+
}
39+
40+
// Cache the check result since it won't change during runtime
41+
const canWriteStackTraceLimit = isStackTraceLimitWritable()
42+
43+
/**
44+
* Get the current `Error.stackTraceLimit` value.
45+
* Returns `undefined` if the property doesn't exist.
46+
*
47+
* @internal
48+
*/
49+
export const getStackTraceLimit = (): number | undefined => (Error as ErrorWithStackTraceLimit).stackTraceLimit
50+
51+
/**
52+
* Safely set `Error.stackTraceLimit` if possible, otherwise no-op.
53+
*
54+
* Accepts `undefined` so a value read via {@link getStackTraceLimit} can be
55+
* restored faithfully.
56+
*
57+
* @internal
58+
*/
59+
export const setStackTraceLimit = (value: number | undefined): void => {
60+
if (canWriteStackTraceLimit) {
61+
;(Error as ErrorWithStackTraceLimit).stackTraceLimit = value
62+
}
63+
}

packages/effect/src/internal/tracer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type * as Tracer from "../Tracer.ts"
2+
import { getStackTraceLimit, setStackTraceLimit } from "./stackTraceLimit.ts"
23

34
export interface ErrorWithStackTraceLimit {
45
stackTraceLimit?: number | undefined
@@ -13,10 +14,10 @@ export const addSpanStackTrace = <A extends Tracer.TraceOptions>(
1314
} else if (options?.captureStackTrace !== undefined && typeof options.captureStackTrace !== "boolean") {
1415
return options
1516
}
16-
const limit = (Error as ErrorWithStackTraceLimit).stackTraceLimit
17-
;(Error as ErrorWithStackTraceLimit).stackTraceLimit = 3
17+
const limit = getStackTraceLimit()
18+
setStackTraceLimit(3)
1819
const traceError = new Error()
19-
;(Error as ErrorWithStackTraceLimit).stackTraceLimit = limit
20+
setStackTraceLimit(limit)
2021
return {
2122
...options,
2223
captureStackTrace: spanCleaner(() => traceError.stack)

packages/effect/src/unstable/ai/Tool.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import * as Context from "../../Context.ts"
1515
import type * as Effect from "../../Effect.ts"
1616
import { constFalse, constTrue, identity } from "../../Function.ts"
17+
import * as StackTraceLimit from "../../internal/stackTraceLimit.ts"
1718
import type * as JsonSchema from "../../JsonSchema.ts"
1819
import { pipeArguments } from "../../Pipeable.ts"
1920
import * as Predicate from "../../Predicate.ts"
@@ -1956,12 +1957,12 @@ function filter(obj: any) {
19561957
*/
19571958
export const unsafeSecureJsonParse = (text: string): unknown => {
19581959
// Performance optimization, see https://github.com/fastify/secure-json-parse/pull/90
1959-
const { stackTraceLimit } = Error
1960-
Error.stackTraceLimit = 0
1960+
const prevLimit = StackTraceLimit.getStackTraceLimit()
1961+
StackTraceLimit.setStackTraceLimit(0)
19611962
try {
19621963
return _parse(text)
19631964
} finally {
1964-
Error.stackTraceLimit = stackTraceLimit
1965+
StackTraceLimit.setStackTraceLimit(prevLimit)
19651966
}
19661967
}
19671968

packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
/** @effect-diagnostics classSelfMismatch:off */
1616
import * as Context from "../../Context.ts"
1717
import * as Effect from "../../Effect.ts"
18+
import { getStackTraceLimit, setStackTraceLimit } from "../../internal/stackTraceLimit.ts"
1819
import * as Layer from "../../Layer.ts"
1920
import { hasProperty } from "../../Predicate.ts"
2021
import type * as Schema from "../../Schema.ts"
@@ -352,10 +353,10 @@ export const Service = <
352353
} | undefined
353354
) => {
354355
const Err = globalThis.Error as any
355-
const limit = Err.stackTraceLimit
356-
Err.stackTraceLimit = 2
356+
const limit = getStackTraceLimit()
357+
setStackTraceLimit(2)
357358
const creationError = new Err()
358-
Err.stackTraceLimit = limit
359+
setStackTraceLimit(limit)
359360

360361
class Service extends Context.Service<Self, any>()(id) {}
361362
const self = Service as any

packages/effect/src/unstable/rpc/RpcMiddleware.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212
import * as Context from "../../Context.ts"
1313
import * as Effect from "../../Effect.ts"
14+
import { getStackTraceLimit, setStackTraceLimit } from "../../internal/stackTraceLimit.ts"
1415
import * as Layer from "../../Layer.ts"
1516
import * as Schema from "../../Schema.ts"
1617
import { Scope } from "../../Scope.ts"
@@ -292,10 +293,10 @@ export const Service = <
292293
}
293294
) => {
294295
const Err = globalThis.Error as any
295-
const limit = Err.stackTraceLimit
296-
Err.stackTraceLimit = 2
296+
const limit = getStackTraceLimit()
297+
setStackTraceLimit(2)
297298
const creationError = new Err()
298-
Err.stackTraceLimit = limit
299+
setStackTraceLimit(limit)
299300

300301
function ServiceClass() {}
301302
const ServiceClass_ = ServiceClass as any as Mutable<AnyService>

0 commit comments

Comments
 (0)