Raised as defense-in-depth when an engine-internal recursive walker exhausts the V8 call stack while populating a freshly-registered derivation's value and dependency set (#936, status post-#956).

The registration path (graph.derived(...)) eagerly evaluates a new derivation to populate its value, dep-set, and the reverse-dep adjacency map. After #670 / #705 / #773 / #956, every walker on the registration and commit-time hot paths is iterative end-to-end:

  • Registration-time eager evaluation runs through computeDerivedIterative (#670), which walks the dep graph via an explicit StackFrame[] rather than recursive calls, restart-on-miss when an upstream is uncomputed.
  • Commit-time Phase D recompute walks the affected sub-graph via an augmented Kahn pass (#705 / #773); each derivation's computeDerived invocation finds every upstream computed by topo order, so the recursive get accessor's lazy-upstream branch is never taken in practice.
  • Lazy first-read via readEntry's !e.computed branch is unreachable on a graph built through graph.derived(...) because every derived is computed eagerly at registration; it remains in the source as a defensive fallback for any future code path that bypasses the registration-time evaluator.

Pre-#956 this error was the expected normal-path failure mode at depth 10k (PR #943 wired the registration walker's catch-arm to convert V8's raw RangeError). Post-#956 the iterative driver lifts the depth ceiling past 12k on Node 22+, so this error becomes a residual-recursion guard rather than the steady-state outcome — kept in the engine because:

  1. A user derived body whose compute itself recurses outside the tracker (e.g. a recursive helper closing over a long dep-chain manually) can still overflow V8 and surface a raw RangeError; converting it preserves the user-facing contract "no public causl API ever crashes the Node process with a raw V8 RangeError".
  2. Stricter --stack-size configurations or non-V8 runtimes may surface stack exhaustion at lower depths; a typed error keeps the DX symmetric across deployment shapes.
  3. The conversion is cheap (one instanceof + String#startsWith check inside the derived(...) catch arm) and pays nothing on the hot path.

The symmetric DX in the bench is RecursiveEvalStackOverflowError (jotai #922, mobx #798, redux #926); those harnesses still gate linear-chain × 10000 upfront because their walkers stayed recursive — the asymmetry vs causl is the post-#956 bench result.

The derived node id whose registration overflow-converted.

The observed chain depth at registration time, when known — otherwise -1. Carried so a caller printing the error message can locate the offending workload without re-deriving the shape from the stack trace.

Hierarchy (View Summary)

Constructors

Properties

cause?: unknown
id: string
kind: "DerivedRegistrationStackOverflow" = ...

Discriminated tag for exhaustive matching.

message: string
name: string = 'DerivedRegistrationStackOverflowError'
scale: number = -1
stack?: string
stackTraceLimit: number

The Error.stackTraceLimit property specifies the number of stack frames collected by a stack trace (whether generated by new Error().stack or Error.captureStackTrace(obj)).

The default value is 10 but may be set to any valid JavaScript number. Changes will affect any stack trace captured after the value has been changed.

If set to a non-number value, or set to a negative number, stack traces will not capture any frames.

Methods

  • Creates a .stack property on targetObject, which when accessed returns a string representing the location in the code at which Error.captureStackTrace() was called.

    const myObject = {};
    Error.captureStackTrace(myObject);
    myObject.stack; // Similar to `new Error().stack`

    The first line of the trace will be prefixed with ${myObject.name}: ${myObject.message}.

    The optional constructorOpt argument accepts a function. If given, all frames above constructorOpt, including constructorOpt, will be omitted from the generated stack trace.

    The constructorOpt argument is useful for hiding implementation details of error generation from the user. For instance:

    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a();

    Parameters

    • targetObject: object
    • OptionalconstructorOpt: Function

    Returns void