Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/frozen-intrinsics-stack-trace-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"effect": patch
"@effect/ai": patch
---

Avoid throwing when `Error.stackTraceLimit` is non-writable (frozen intrinsics / SES / deterministic sandboxes such as Temporal).

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.
23 changes: 21 additions & 2 deletions packages/ai/ai/src/Tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,22 @@ function filter(obj: any) {
return obj
}

// Utility for safely manipulating `Error.stackTraceLimit` in environments where
// intrinsics may be frozen (e.g., SES / hardened JavaScript). Duplicated here
// because `effect`'s internal helper is not part of its public API. When the
// property is non-writable, mutating it throws, so this degrades to a no-op.
const isStackTraceLimitWritable = (): boolean => {
const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit")
if (desc === undefined) {
return Object.isExtensible(Error)
}
return Object.prototype.hasOwnProperty.call(desc, "writable")
? desc.writable === true
: desc.set !== undefined
}

const canWriteStackTraceLimit = isStackTraceLimitWritable()

/**
* **Unsafe**: This function will throw an error if an insecure property is
* found in the parsed JSON or if the provided JSON text is not parseable.
Expand All @@ -1515,12 +1531,15 @@ function filter(obj: any) {
*/
export const unsafeSecureJsonParse = (text: string): unknown => {
// Performance optimization, see https://github.com/fastify/secure-json-parse/pull/90
const { stackTraceLimit } = Error
if (!canWriteStackTraceLimit) {
return _parse(text)
}
const prevLimit = Error.stackTraceLimit
Error.stackTraceLimit = 0
try {
return _parse(text)
} finally {
Error.stackTraceLimit = stackTraceLimit
Error.stackTraceLimit = prevLimit
}
}

Expand Down
31 changes: 16 additions & 15 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import * as option_ from "./internal/option.js"
import * as query from "./internal/query.js"
import * as runtime_ from "./internal/runtime.js"
import * as schedule_ from "./internal/schedule.js"
import * as StackTraceLimit from "./internal/stackTraceLimit.js"
import * as internalTracer from "./internal/tracer.js"
import type * as Layer from "./Layer.js"
import type * as LogLevel from "./LogLevel.js"
Expand Down Expand Up @@ -13515,10 +13516,10 @@ export const Tag: <const Id extends string>(id: Id) => <
: [X] extends [PromiseLike<infer A>] ? Effect<A, Cause.UnknownException, Self>
: Effect<X, never, Self>
} = (id) => () => {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const creationError = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)
function TagClass() {}
Object.setPrototypeOf(TagClass, TagProto)
TagClass.key = id
Expand Down Expand Up @@ -13674,10 +13675,10 @@ export const Service: <Self = never>() => [Self] extends [never] ? MissingSelfGe
return function() {
const [id, maker] = arguments
const proxy = "accessors" in maker ? maker["accessors"] : false
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const creationError = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)

let patchState: "unchecked" | "plain" | "patched" = "unchecked"
const TagClass: any = function(this: any, service: any) {
Expand Down Expand Up @@ -14634,16 +14635,16 @@ export const fn:
name: string,
options?: Tracer.SpanOptions
) => fn.Gen & fn.NonGen) = function(nameOrBody: Function | string, ...pipeables: Array<any>) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limitDef = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const errorDef = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limitDef)
if (typeof nameOrBody !== "string") {
return defineLength(nameOrBody.length, function(this: any, ...args: Array<any>) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limitCall = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const errorCall = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limitCall)
return fnApply({
self: this,
body: nameOrBody,
Expand All @@ -14665,10 +14666,10 @@ export const fn:
body.length,
({
[name](this: any, ...args: Array<any>) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limitCall = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const errorCall = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limitCall)
return fnApply({
self: this,
body,
Expand Down
7 changes: 4 additions & 3 deletions packages/effect/src/LayerMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as Effect from "./Effect.js"
import * as FiberRefsPatch from "./FiberRefsPatch.js"
import { identity } from "./Function.js"
import * as core from "./internal/core.js"
import * as StackTraceLimit from "./internal/stackTraceLimit.js"
import * as Layer from "./Layer.js"
import * as RcMap from "./RcMap.js"
import * as Runtime from "./Runtime.js"
Expand Down Expand Up @@ -357,10 +358,10 @@ export const Service = <Self>() =>
Options extends { readonly dependencies: ReadonlyArray<any> } ? Options["dependencies"][number] : never
> => {
const Err = globalThis.Error as any
const limit = Err.stackTraceLimit
Err.stackTraceLimit = 2
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const creationError = new Err()
Err.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)

function TagClass() {}
const TagClass_ = TagClass as any as Mutable<TagClass<Self, Id, string, any, any, any, any, any>>
Expand Down
7 changes: 4 additions & 3 deletions packages/effect/src/Micro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { format, NodeInspectSymbol, toStringUnknown } from "./Inspectable.js"
import * as InternalContext from "./internal/context.js"
import * as doNotation from "./internal/doNotation.js"
import { StructuralPrototype } from "./internal/effectable.js"
import * as StackTraceLimit from "./internal/stackTraceLimit.js"
import * as Option from "./Option.js"
import type { Pipeable } from "./Pipeable.js"
import { pipeArguments } from "./Pipeable.js"
Expand Down Expand Up @@ -2984,10 +2985,10 @@ export const withTrace: {
(name: string): <A, E, R>(self: Micro<A, E, R>) => Micro<A, E, R>
<A, E, R>(self: Micro<A, E, R>, name: string): Micro<A, E, R>
} = function() {
const prevLimit = globalThis.Error.stackTraceLimit
globalThis.Error.stackTraceLimit = 2
const prevLimit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const error = new globalThis.Error()
globalThis.Error.stackTraceLimit = prevLimit
StackTraceLimit.setStackTraceLimit(prevLimit)
function generate(name: string, cause: MicroCause<any>) {
const stack = error.stack
if (!stack) {
Expand Down
9 changes: 5 additions & 4 deletions packages/effect/src/internal/cause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { AnySpan, Span } from "../Tracer.js"
import type * as Types from "../Types.js"
import { getBugErrorMessage } from "./errors.js"
import * as OpCodes from "./opCodes/cause.js"
import * as StackTraceLimit from "./stackTraceLimit.js"

// -----------------------------------------------------------------------------
// Models
Expand Down Expand Up @@ -898,19 +899,19 @@ const renderErrorCause = (cause: Cause.PrettyError, prefix: string) => {
/** @internal */
export const makePrettyError = (originalError: unknown): Cause.PrettyError => {
const originalErrorIsObject = typeof originalError === "object" && originalError !== null
const prevLimit = Error.stackTraceLimit
Error.stackTraceLimit = 1
const prevLimit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(1)
const error = new Error(
prettyErrorMessage(originalError),
originalErrorIsObject && "cause" in originalError && typeof originalError.cause !== "undefined"
? { cause: makePrettyError(originalError.cause) }
: undefined
) as Types.Mutable<Cause.PrettyError>
Error.stackTraceLimit = prevLimit
StackTraceLimit.setStackTraceLimit(prevLimit)
if (error.message === "") {
error.message = "An error has occurred"
}
Error.stackTraceLimit = prevLimit
StackTraceLimit.setStackTraceLimit(prevLimit)
error.name = originalError instanceof Error ? originalError.name : "Error"
if (originalErrorIsObject) {
if (spanSymbol in originalError) {
Expand Down
19 changes: 10 additions & 9 deletions packages/effect/src/internal/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type * as STM from "../STM.js"
import type { NoInfer } from "../Types.js"
import { EffectPrototype, effectVariance } from "./effectable.js"
import * as option from "./option.js"
import * as StackTraceLimit from "./stackTraceLimit.js"

/** @internal */
export const TagTypeId: C.TagTypeId = Symbol.for("effect/Context/Tag") as C.TagTypeId
Expand Down Expand Up @@ -67,10 +68,10 @@ export const ReferenceProto: any = {

/** @internal */
export const makeGenericTag = <Identifier, Service = Identifier>(key: string): C.Tag<Identifier, Service> => {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const creationError = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)
const tag = Object.create(TagProto)
Object.defineProperty(tag, "stack", {
get() {
Expand All @@ -83,10 +84,10 @@ export const makeGenericTag = <Identifier, Service = Identifier>(key: string): C

/** @internal */
export const Tag = <const Id extends string>(id: Id) => <Self, Shape>(): C.TagClass<Self, Id, Shape> => {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const creationError = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)

function TagClass() {}
Object.setPrototypeOf(TagClass, TagProto)
Expand All @@ -104,10 +105,10 @@ export const Reference = <Self>() =>
<const Id extends string, Service>(id: Id, options: {
readonly defaultValue: () => Service
}): C.ReferenceClass<Self, Id, Service> => {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const creationError = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)

function ReferenceClass() {}
Object.setPrototypeOf(ReferenceClass, ReferenceProto)
Expand Down
7 changes: 4 additions & 3 deletions packages/effect/src/internal/core-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import * as fiberRefsPatch from "./fiberRefs/patch.js"
import type { FiberRuntime } from "./fiberRuntime.js"
import * as metricLabel from "./metric/label.js"
import * as runtimeFlags from "./runtimeFlags.js"
import * as StackTraceLimit from "./stackTraceLimit.js"
import * as internalTracer from "./tracer.js"

/* @internal */
Expand Down Expand Up @@ -2252,10 +2253,10 @@ export const functionWithSpan = <Args extends Array<any>, Ret extends Effect.Eff
(function(this: any) {
let captureStackTrace: LazyArg<string | undefined> | boolean = options.captureStackTrace ?? false
if (options.captureStackTrace !== false) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const error = new Error()
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)
let cache: false | string = false
captureStackTrace = () => {
if (cache !== false) {
Expand Down
7 changes: 4 additions & 3 deletions packages/effect/src/internal/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as OpCodes from "./opCodes/layer.js"
import * as ref from "./ref.js"
import * as runtime from "./runtime.js"
import * as runtimeFlags from "./runtimeFlags.js"
import * as StackTraceLimit from "./stackTraceLimit.js"
import * as synchronized from "./synchronizedRef.js"
import * as tracer from "./tracer.js"

Expand Down Expand Up @@ -693,10 +694,10 @@ const mockImpl = <I, S extends object>(tag: Context.Tag<I, S>, service: Layer.Pa
if (prop in target) {
return target[prop as keyof S]
}
const prevLimit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const prevLimit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(2)
const error = new Error(`${tag.key}: Unimplemented method "${prop.toString()}"`)
Error.stackTraceLimit = prevLimit
StackTraceLimit.setStackTraceLimit(prevLimit)
error.name = "UnimplementedError"
return makeUnimplemented(error)
},
Expand Down
13 changes: 7 additions & 6 deletions packages/effect/src/internal/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import * as FiberRuntime from "./fiberRuntime.js"
import * as fiberScope from "./fiberScope.js"
import * as OpCodes from "./opCodes/effect.js"
import * as runtimeFlags from "./runtimeFlags.js"
import * as StackTraceLimit from "./stackTraceLimit.js"
import * as supervisor_ from "./supervisor.js"

const makeDual = <Args extends Array<any>, Return>(
Expand Down Expand Up @@ -179,10 +180,10 @@ class AsyncFiberExceptionImpl<A, E = never> extends Error implements Runtime.Asy
}

const asyncFiberException = <A, E>(fiber: Fiber.RuntimeFiber<A, E>): Runtime.AsyncFiberException<A, E> => {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 0
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(0)
const error = new AsyncFiberExceptionImpl(fiber)
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)
return error
}

Expand Down Expand Up @@ -230,10 +231,10 @@ class FiberFailureImpl extends Error implements Runtime.FiberFailure {

/** @internal */
export const fiberFailure = <E>(cause: Cause.Cause<E>): Runtime.FiberFailure => {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 0
const limit = StackTraceLimit.getStackTraceLimit()
StackTraceLimit.setStackTraceLimit(0)
const error = new FiberFailureImpl(cause)
Error.stackTraceLimit = limit
StackTraceLimit.setStackTraceLimit(limit)
return error
}

Expand Down
60 changes: 60 additions & 0 deletions packages/effect/src/internal/stackTraceLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Utility for safely manipulating `Error.stackTraceLimit` in environments
* where intrinsics may be frozen (e.g., SES / hardened JavaScript or
* deterministic sandboxes). When the property is non-writable, mutating it
* throws, so all manipulation here degrades to a best-effort, silent no-op.
*
* Mirrors the guard Node uses internally:
* https://github.com/nodejs/node/blob/e77694631f1642c302f664703197b5aabc65b482/lib/internal/errors.js#L246
*
* The error is constructed inline at each call site (rather than inside a
* closure here) so the captured stack trace keeps pointing at the real caller
* instead of this module.
*
* @internal
*/
const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor
const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty
const ObjectIsExtensible = Object.isExtensible

/**
* Check if `Error.stackTraceLimit` is writable.
* Returns `false` if the property is frozen, non-writable, or `Error` is non-extensible.
*
* @internal
*/
export const isStackTraceLimitWritable = (): boolean => {
const desc = ObjectGetOwnPropertyDescriptor(Error, "stackTraceLimit")
if (desc === undefined) {
return ObjectIsExtensible(Error)
}

return ObjectPrototypeHasOwnProperty.call(desc, "writable")
? desc.writable === true
: desc.set !== undefined
}

// Cache the check result since it won't change during runtime
const canWriteStackTraceLimit = isStackTraceLimitWritable()

/**
* Get the current `Error.stackTraceLimit` value.
* Returns `undefined` if the property doesn't exist.
*
* @internal
*/
export const getStackTraceLimit = (): number | undefined => Error.stackTraceLimit

/**
* Safely set `Error.stackTraceLimit` if possible, otherwise no-op.
*
* Accepts `undefined` so a value read via {@link getStackTraceLimit} can be
* restored faithfully.
*
* @internal
*/
export const setStackTraceLimit = (value: number | undefined): void => {
if (canWriteStackTraceLimit) {
Error.stackTraceLimit = value as number
}
}
Loading