#12380·swc

es/minifier: class field callback shared across instances after helper inlining

Author: leosvelperezCreated Sep 16, 2026Updated Sep 16, 2026

Describe the bug

A class field initializer passes an arrow function to a helper. The minifier inlines the helper and assigns the arrow to a var declared outside the class.

An instance field initializer runs once per instance, so each instance should keep its own arrow. After minification all instances share that one var. Each new Counter() overwrites it, so every instance's closure calls the arrow stored by the last instance.

It reproduces when the class is in a module (isModule: true), when compress.toplevel is set, or when the code is inside a function, such as an IIFE or a bundler's output wrapper. When the same input is top-level script code without toplevel, the output is correct. The smallest compress config I found that triggers it is { "defaults": false, "inline": true }.

#10824 and #11368 (closed by #11399) reported the same symptom with other shapes: an arrow function field with a default parameter, and a member expression argument, wrap(this.props.cb). #12128 is a similar case where a helper's local variable was hoisted out of a function called in a loop.

Input code

typescript
function wrap(cb) {
  return () => cb();
}

class Counter {
  static total = 0;
  id = ++Counter.total;
  read = wrap(() => this.id);
}

const counters = [new Counter(), new Counter(), new Counter()];
console.log(counters.map((c) => c.read()).join(","));

Config

json
{
  "jsc": {
    "parser": {
      "syntax": "ecmascript"
    },
    "target": "es2022",
    "minify": {
      "compress": {},
      "mangle": false
    }
  },
  "minify": true,
  "isModule": true
}

Link to the code that reproduces this issue

https://play.swc.rs/?version=1.16.2&code=H4sIAAAAAAAAE32PwQ7CIBBE73zFxBObNsQ7wYufYTxQiopBMLBND8Z%2FN6Xt1eNmZt7s3KbkOOSEudi3dAPhI4DieSoJkmBOcIMkLb5CuGhrxTlPiX1pvsqWgwNnthEGRy2AMMKg6zabappuTLsorWcF8yNUFcYNnlNluDVVYXBJft7LJPX4d151y%2BfoVcx3uVPUa%2Bly6wq1PCCJ1DOHJA%2F9gUiLH90ipOv%2FAAAA&config=H4sIAAAAAAAAE02MMQrDMAxFd59CaO5QPPoOPYRwleASO0ZSoCHk7sWOaTvq6b93OAB8acQAhwMAwEqiLN8bAHUvRm8MgBwzaZRUDfvzvF2OkcxsfaH%2B7j0OnlNJ0%2F7fimuuwqqNDbvtqMwLY4CJFuUr7Ub%2BFzHZuJOkj%2FW5daExd34ADfSY7sYAAAA%3D

SWC Info output

    Operating System:
        Platform: darwin
        Arch: arm64
        Machine Type: arm64
        Version: Darwin Kernel Version 25.6.0: Fri Jul 31 19:18:48 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6020
        CPU: (12 cores)
            Models: Apple M2 Max

    Binaries:
        Node: 24.15.0
        npm: 11.16.0
        Yarn: 4.12.0
        pnpm: 10.34.0

    Relevant Packages:
        @swc/core: 1.16.2
        @swc/helpers: N/A
        @swc/types: 0.1.28

Expected behavior

Running the input and running the minified output should both print:

1,2,3

Inlining wrap is fine, but each evaluation of the field initializer needs its own binding for the argument. Keeping an IIFE preserves that, for example:

javascript
class Counter{static total=0;id=++Counter.total;read=(cb=>()=>cb())(()=>this.id)}console.log([new Counter,new Counter,new Counter].map(c=>c.read()).join(","));

Actual behavior

SWC produces:

javascript
var cb;class Counter{static total=0;id=++Counter.total;read=(cb=()=>this.id,()=>cb())}console.log([new Counter,new Counter,new Counter].map(c=>c.read()).join(","));

This prints:

3,3,3

cb is a single var outside the class. Each instance assigns its own arrow to it, so by the time read() runs, cb holds the third instance's arrow and all three calls return 3.

Version

1.16.2 (also reproduced on 1.16.4-nightly-20260913.1)

Additional context

We hit this through Rspack's SwcJsMinimizerRspackPlugin. It reproduces there on Rspack 1.3.5, 1.6.8, 2.0.4 and 2.2.4, with module concatenation on or off and with module unset or false, because Rspack's output places the code inside a function. The downstream report is https://github.com/nrwl/nx/issues/37050. There, an Angular component passes an options callback to injectVirtualizer from @tanstack/angular-virtual in a field initializer, and in a production build every instance's virtualizer runs the options callback of the last instance created.

For this input, setting inline: 0 alone does not avoid the bug, and neither does reduce_funcs: false alone. Setting both does, and that is the workaround proposed in https://github.com/nrwl/nx/pull/37066. With both set, the output keeps the helper as an IIFE: read=(function(cb){return()=>cb()})(()=>this.id).