Describe the bug
When SWC's compress option is enabled, a class that has a class property initialized by calling a function that returns a closure will have that closure incorrectly capture values from the last instantiated instance instead of its own instance.
Input code
/**
* SWC Compress Bug - Playground Reproduction
* ==========================================
*
*
* Steps:
* 1. Run the output → logs "BUG: expected A, got B"
* 2. Disable compress and run again → logs "OK: got A"
*
* Bug: When SWC's compress is enabled, a class property that calls
* a function which returns a closure will have that closure capture
* values from the LAST instance instead of its own.
*/
const wrap = (cb) => () => cb();
class Base {
constructor(props) {
this.props = props;
}
}
class C extends Base {
fn = wrap(this.props.cb);
}
// Test
const a = new C({cb: () => 'A'});
const b = new C({cb: () => 'B'});
const result = a.fn();
console.log(result === 'A' ? 'OK: got A' : 'BUG: expected A, got ' + result);
Config
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
},
"minify": {
"compress": {
"arguments": false,
"arrows": true,
"booleans": true,
"booleans_as_integers": false,
"collapse_vars": true,
"comparisons": true,
"computed_props": true,
"conditionals": true,
"dead_code": true,
"directives": true,
"drop_console": false,
"drop_debugger": true,
"evaluate": true,
"expression": false,
"hoist_funs": false,
"hoist_props": true,
"hoist_vars": false,
"if_return": true,
"join_vars": true,
"keep_classnames": false,
"keep_fargs": true,
"keep_fnames": false,
"keep_infinity": false,
"loops": true,
"negate_iife": true,
"properties": true,
"reduce_funcs": false,
"reduce_vars": false,
"side_effects": true,
"switches": true,
"typeofs": true,
"unsafe": false,
"unsafe_arrows": false,
"unsafe_comps": false,
"unsafe_Function": false,
"unsafe_math": false,
"unsafe_symbols": false,
"unsafe_methods": false,
"unsafe_proto": false,
"unsafe_regexp": false,
"unsafe_undefined": false,
"unused": true,
"const_to_let": true,
"pristine_globals": true
},
"mangle": false,
},
},
"module": {
"type": "es6"
},
"minify": false,
"env": {
"targets": "> 0.5%, last 3 major versions, not dead, Firefox ESR"
},
"isModule": true,
}
Link to the code that reproduces this issue
https://play.swc.rs/?version=1.15.3&code=H4sIAAAAAAAAA42STW7bMBCF9zrFgzeS3VRGu5ShFpYLdNECLeIUWdPU6AdgSIE%2FUYPA2x6gR%2BxJOmTkJossKggSMXrv4xuOtptNhg2OtwcczN1kyTk0ocdbfFfiobcm6BbXNFnTBulHo6O6%2Fu%2BL1U83jp4mV8XVuxLXQcMPBBP8FDz%2B%2FPoNZXqHVfPjcwX6OZH01GJ%2Fhd54NKtoe1%2Fi0%2BjESRHkJangcJZZohejfoH59qVKzv1q2Z1bqnA7kI6d5u6ZMDqQjtD2CgJSCa5xsxNZ%2F8ARhYcUSrnIEOiCTmeAeRjlAEs%2BWO2Sz7hgCfOoFAZxT4t1KUsxsZIi5F6oQA6dNXfpBL7ujzcYtfNCS0oLEi1Mh9E7mFmXbNpmmTT8BbMVE2oU8rRG%2FQFFespTsd6xIkVvhCM8ZkAyWB6ZsUXsx61TGbzp6MpUYVJ677h%2Bzs4XxIHP35NuX8A6zdq4efHsLjnELrq2W9yQ80tEwUpNMw7FozxVS8R8n59Z%2FKQ4vapokmKR8GSC8qwTZaeLS90oKnm8xeVrXUcwPiL%2FN%2B4cFaNe%2B4dyvFmwjPsL8m7U8vUCAAA%3D&config=H4sIAAAAAAAAA32UyY7bMAxA7%2F0KwcDcgqJA0Tn00Fvn1kv7AYJiUY5SWzREKpNgkH8vZWdrQufihY%2BLxO3jkzHNltrmu%2FmQT%2FkZXSbIl3%2BR0CGx24uk4cMI1OY4crM6U6aKOBeYRcf51QwxxXC49dPiMGYgupGJ1OWuDJC4ioPrCVa3LOM7%2Fed%2Bkq8Re3DpCbGObEwMHWTNcYt970YCu3NZ8VJP6nIk1EJUWBi8HTOOKk8%2BcsQkMR%2BpB%2Bdtix4UFDO0HHegmUksMUsk11PuM2EP69J1U%2B3urGHn%2BuJYiQn7qSRyWsXrBiOxDSVpKZzhQg5meEruvWUMNgOXnB7tthjTQk3%2BAkgGekeU3ACa30kjSD8tWYenljEFaVk%2BKLxH9ZYJOkmqjTEoma2ZgcxRq2YGX1qomW2145zwQvooerAQgvSK4preI7cbLWgdXgwKkPq6oHXVDOxlChd4HYgn%2BE1uyXqDnTQGx5tlSodhjf2TAAPwBv0TBSkF4zLOsiX24zIvyYO0BnhVpdAEHpeADACj7YG13pDxEI%2B263F9XRMnheNltw4udffzPtHp0Qzoy4RPq7tWuK5poNfmqnTewxcfDaTdjZEMDEyd1PwwXz5%2Fe1kZGTI2X83gtpjNTjaoVI9WJiGbur5W5k02VcC9%2Bfnn9zVQpF%2Fn88zXPf4DBX4mo10GAAA%3D
SWC Info output
Operating System:
Platform: darwin
Arch: arm64
Machine Type: arm64
Version: Darwin Kernel Version 24.6.0: Wed Oct 15 21:12:06 PDT 2025; root:xnu-11417.140.69.703.14~1/RELEASE_ARM64_T6000
CPU: (10 cores)
Models: Apple M1 Max
Binaries:
Node: 20.19.0
npm: 10.8.2
Yarn: N/A
pnpm: N/A
Relevant Packages:
@swc/core: 1.15.3
@swc/helpers: 0.5.11
@swc/types: 0.1.25
typescript: 5.9.3
next: 14.2.32
SWC Config:
output: N/A
.swcrc path: N/A
Next.js info:
output: N/A
Expected behavior
When calling a.fn(), the closure should invoke the callback from instance a, returning 'A'.
Output should be: OK: got A
Actual behavior
With compress enabled, the closure incorrectly captures the callback from the last instantiated instance (b), returning 'B' instead.
Output is: BUG: expected A, got B
Version
1.15.3
Additional context
Conditions Required to Trigger
- Class extends a base class that sets a property (e.g.,
this.props) in its constructor
- Subclass has a class property initialized by calling an external function
- That function returns a closure over its parameter
Root Cause
When compress inlines the wrap function, it incorrectly hoists the cb parameter to module scope:
Input:
const wrap = (cb) => () => cb();
class C extends Base {
fn = wrap(this.props.cb);
}
Compressed output:
let cb; // <-- Hoisted to module scope!
class C extends Base {
fn = (cb = this.props.cb, ()=>cb());
}
The inlined code uses the comma operator to assign this.props.cb to the shared cb variable, then returns a closure over it. Since cb is at module scope, all instances share the same variable, and each instantiation overwrites it.
Correct behavior would be to keep cb scoped per-invocation, something like:
class C extends Base {
fn = ((cb) => () => cb())(this.props.cb);
}
Notes
- The bug does not occur when the closure is created inline in the class property
- The bug does not occur without compress enabled
Describe the bug
When SWC's
compressoption is enabled, a class that has a class property initialized by calling a function that returns a closure will have that closure incorrectly capture values from the last instantiated instance instead of its own instance.Input code
Config
{ "jsc": { "parser": { "syntax": "typescript", "tsx": true, }, "minify": { "compress": { "arguments": false, "arrows": true, "booleans": true, "booleans_as_integers": false, "collapse_vars": true, "comparisons": true, "computed_props": true, "conditionals": true, "dead_code": true, "directives": true, "drop_console": false, "drop_debugger": true, "evaluate": true, "expression": false, "hoist_funs": false, "hoist_props": true, "hoist_vars": false, "if_return": true, "join_vars": true, "keep_classnames": false, "keep_fargs": true, "keep_fnames": false, "keep_infinity": false, "loops": true, "negate_iife": true, "properties": true, "reduce_funcs": false, "reduce_vars": false, "side_effects": true, "switches": true, "typeofs": true, "unsafe": false, "unsafe_arrows": false, "unsafe_comps": false, "unsafe_Function": false, "unsafe_math": false, "unsafe_symbols": false, "unsafe_methods": false, "unsafe_proto": false, "unsafe_regexp": false, "unsafe_undefined": false, "unused": true, "const_to_let": true, "pristine_globals": true }, "mangle": false, }, }, "module": { "type": "es6" }, "minify": false, "env": { "targets": "> 0.5%, last 3 major versions, not dead, Firefox ESR" }, "isModule": true, }Link to the code that reproduces this issue
https://play.swc.rs/?version=1.15.3&code=H4sIAAAAAAAAA42STW7bMBCF9zrFgzeS3VRGu5ShFpYLdNECLeIUWdPU6AdgSIE%2FUYPA2x6gR%2BxJOmTkJossKggSMXrv4xuOtptNhg2OtwcczN1kyTk0ocdbfFfiobcm6BbXNFnTBulHo6O6%2Fu%2BL1U83jp4mV8XVuxLXQcMPBBP8FDz%2B%2FPoNZXqHVfPjcwX6OZH01GJ%2Fhd54NKtoe1%2Fi0%2BjESRHkJangcJZZohejfoH59qVKzv1q2Z1bqnA7kI6d5u6ZMDqQjtD2CgJSCa5xsxNZ%2F8ARhYcUSrnIEOiCTmeAeRjlAEs%2BWO2Sz7hgCfOoFAZxT4t1KUsxsZIi5F6oQA6dNXfpBL7ujzcYtfNCS0oLEi1Mh9E7mFmXbNpmmTT8BbMVE2oU8rRG%2FQFFespTsd6xIkVvhCM8ZkAyWB6ZsUXsx61TGbzp6MpUYVJ677h%2Bzs4XxIHP35NuX8A6zdq4efHsLjnELrq2W9yQ80tEwUpNMw7FozxVS8R8n59Z%2FKQ4vapokmKR8GSC8qwTZaeLS90oKnm8xeVrXUcwPiL%2FN%2B4cFaNe%2B4dyvFmwjPsL8m7U8vUCAAA%3D&config=H4sIAAAAAAAAA32UyY7bMAxA7%2F0KwcDcgqJA0Tn00Fvn1kv7AYJiUY5SWzREKpNgkH8vZWdrQufihY%2BLxO3jkzHNltrmu%2FmQT%2FkZXSbIl3%2BR0CGx24uk4cMI1OY4crM6U6aKOBeYRcf51QwxxXC49dPiMGYgupGJ1OWuDJC4ioPrCVa3LOM7%2Fed%2Bkq8Re3DpCbGObEwMHWTNcYt970YCu3NZ8VJP6nIk1EJUWBi8HTOOKk8%2BcsQkMR%2BpB%2Bdtix4UFDO0HHegmUksMUsk11PuM2EP69J1U%2B3urGHn%2BuJYiQn7qSRyWsXrBiOxDSVpKZzhQg5meEruvWUMNgOXnB7tthjTQk3%2BAkgGekeU3ACa30kjSD8tWYenljEFaVk%2BKLxH9ZYJOkmqjTEoma2ZgcxRq2YGX1qomW2145zwQvooerAQgvSK4preI7cbLWgdXgwKkPq6oHXVDOxlChd4HYgn%2BE1uyXqDnTQGx5tlSodhjf2TAAPwBv0TBSkF4zLOsiX24zIvyYO0BnhVpdAEHpeADACj7YG13pDxEI%2B263F9XRMnheNltw4udffzPtHp0Qzoy4RPq7tWuK5poNfmqnTewxcfDaTdjZEMDEyd1PwwXz5%2Fe1kZGTI2X83gtpjNTjaoVI9WJiGbur5W5k02VcC9%2Bfnn9zVQpF%2Fn88zXPf4DBX4mo10GAAA%3D
SWC Info output
Expected behavior
When calling
a.fn(), the closure should invoke the callback from instancea, returning'A'.Output should be:
OK: got AActual behavior
With compress enabled, the closure incorrectly captures the callback from the last instantiated instance (
b), returning'B'instead.Output is:
BUG: expected A, got BVersion
1.15.3
Additional context
Conditions Required to Trigger
this.props) in its constructorRoot Cause
When compress inlines the
wrapfunction, it incorrectly hoists thecbparameter to module scope:Input:
Compressed output:
The inlined code uses the comma operator to assign
this.props.cbto the sharedcbvariable, then returns a closure over it. Sincecbis at module scope, all instances share the same variable, and each instantiation overwrites it.Correct behavior would be to keep cb scoped per-invocation, something like:
Notes