Skip to content

fix: do not read this before super() in wrapped constructors - #101

Open
isaacs wants to merge 1 commit into
nodejs:mainfrom
isaacs:isaacs/super-handling-fixes
Open

isaacs wants to merge 1 commit into
nodejs:mainfrom
isaacs:isaacs/super-handling-fixes

Conversation

@isaacs

@isaacs isaacs commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

A derived constructor leaves this unbound until super() returns. The wrapper read this in two places that can theoretically run before that point.

Bun rejected both. Node rejected one, and accepted the other on every path except the error path. On both engines, however, the transform was incorrect.

This showed up loudly in Sentry's instrumentation of Hono on Bun.

See: getsentry/sentry-javascript#24371

In JavaScriptCore an arrow function keeps the value of this that it read when it was entered. The wrapper moves the original body into a nested arrow, so super() now runs while the wrapper's own runStores callback is on the stack. That callback keeps the unbound value, and the finally block throws when it reads this:

ReferenceError: 'super()' must be called in derived
constructor before accessing |this| or returning non-object.

V8 re-reads this from the environment, so Node did not show this.

captureSelfAtSuper now rewrites each super(...) call in a constructor body to also record this, and the wrapper reads that variable to fill in message.self:

let tr_ch_apm$Undici_constructor$self;
...
(super(val), tr_ch_apm$Undici_constructor$self = this);
...
__apm$ctx.self ??= tr_ch_apm$Undici_constructor$self;

super(...) already evaluates to the newly bound this, so the sequence expression returns the same value the call did. The read now sits immediately after super(), which makes the order static. The variable name carries the channel, so a second channel that wraps the same constructor writes its own binding instead of shadowing the first. A super() call inside a nested class or a nested non-arrow function belongs to that function, so it keeps its original form.

This also stops the wrapper from hiding an error, which is a pre-existing bug in Node.js related to this. A throw before super() used to surface a ReferenceError in place of the original error. The recorded variable is undefined when super() never ran, so the original error now propagates. The correct fix for this is to statically ensure that there is no way to access this before calling super().

traceFunction no longer calls wrapSuper when the body lands in an arrow function. An arrow inherits super from its enclosing method, so such a body keeps working without a rewrite. The rewrite was wrong for a constructor: it hoists a super['x'] capture to the top of the constructor, above the body's own super() call, and that capture uses this as the receiver. As a result super.method() in a derived constructor failed on Node and on Bun. Methods and generators still get the rewrite, because a real function wrapper does lose the binding.

The tests now run every fixture under Bun as well, when Bun is installed. Bun is not an officially supported engine, but people do run this code under it, and its stricter parsing can help surface latent bugs.

Two fixtures are added. constructor_self_cjs covers message.self and the throw before super(). The fixture constructor_super_method_cjs covers super.method() in a derived constructor.

A derived constructor leaves `this` unbound until `super()` returns. The
wrapper read `this` in two places that can theoretically run before that
point.

Bun rejected both. Node rejected one, and accepted the other on every
path except the error path. On both engines, however, the transform was
incorrect.

This showed up loudly in Sentry's instrumentation of Hono on Bun.

See: getsentry/sentry-javascript#24371

In JavaScriptCore an arrow function keeps the value of `this` that it
read when it was entered. The wrapper moves the original body into a
nested arrow, so `super()` now runs while the wrapper's own `runStores`
callback is on the stack. That callback keeps the unbound value, and the
`finally` block throws when it reads `this`:

```
ReferenceError: 'super()' must be called in derived
constructor before accessing |this| or returning non-object.
```

V8 re-reads `this` from the environment, so Node did not show this.

`captureSelfAtSuper` now rewrites each `super(...)` call in a
constructor body to also record `this`, and the wrapper reads that
variable to fill in `message.self`:

```js
let tr_ch_apm$Undici_constructor$self;
...
(super(val), tr_ch_apm$Undici_constructor$self = this);
...
__apm$ctx.self ??= tr_ch_apm$Undici_constructor$self;
```

`super(...)` already evaluates to the newly bound `this`, so the
sequence expression returns the same value the call did. The read now
sits immediately after `super()`, which makes the order static. The
variable name carries the channel, so a second channel that wraps the
same constructor writes its own binding instead of shadowing the first.
A `super()` call inside a nested class or a nested non-arrow function
belongs to that function, so it keeps its original form.

This also stops the wrapper from hiding an error, which is a
pre-existing bug in Node.js related to this. A throw before `super()`
used to surface a `ReferenceError` in place of the original error. The
recorded variable is `undefined` when `super()` never ran, so the
original error now propagates. The correct fix for this is to statically
ensure that there is no way to access `this` before calling `super()`.

`traceFunction` no longer calls `wrapSuper` when the body lands in an
arrow function. An arrow inherits `super` from its enclosing method, so
such a body keeps working without a rewrite. The rewrite was wrong for a
constructor: it hoists a `super['x']` capture to the top of the
constructor, above the body's own `super()` call, and that capture uses
`this` as the receiver. As a result `super.method()` in a derived
constructor failed on Node and on Bun. Methods and generators still get
the rewrite, because a real `function` wrapper does lose the binding.

The tests now run every fixture under Bun as well, when Bun is
installed. Bun is not an officially supported engine, but people do run
this code under it, and its stricter parsing can help surface latent
bugs.

Two fixtures are added. `constructor_self_cjs` covers `message.self` and
the throw before `super()`. The fixture `constructor_super_method_cjs`
covers `super.method()` in a derived constructor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@isaacs

isaacs commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Simplified probe file that shows the issue being discussed, so it's easier to see what's going on: https://gist.github.com/isaacs/425788a3e645076e59ca7c59f65d0690

@isaacs
isaacs requested review from bizob2828 and jsumners-nr and removed request for jsumners-nr September 17, 2026 18:58
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.

1 participant