fix(minifier): preserve catch binding with direct eval (#22221)

Given:
```
try {
  throw "error"
} catch (e) {
  eval('console.log(e)')
}
```

The expected runtime behaviour is
```
"error"
```

instead, runtime behaviour is:
```
Uncaught ReferenceError: e is not defined
    at eval (eval at <anonymous> (REPL5:4:3), <anonymous>:1:13)
```

This is because we remove the catch parameter, as we see no references to it, despite it being referenced in the `eval` call.

This PR adds a check to see if we are in a direct eval scope, and bails removing the catch parameter if this is the case.
This commit is contained in:
camc314 committed 2026-05-07 11:20:32 +00:00
1 parent 0e13d17c8b
commit 73b4f40d23
2 files changed
+4

No files matched your search

@@ -1674,6 +1674,8 @@ impl<'a> PeepholeOptimizations {
&& let Some(param) = &catch.param
&& let BindingPattern::BindingIdentifier(ident) = &param.pattern
&& (catch.body.body.is_empty() || ctx.scoping().symbol_is_unused(ident.symbol_id()))
// Direct eval can reference the catch parameter even when static analysis sees no use.
&& !ctx.scoping().scope_flags(catch.scope_id()).contains_direct_eval()
// Don't remove catch parameter when the body has a `var` with the same name.
// In `catch (e) { var e = x }`, `var e` hoists to function scope but the assignment
// targets the catch parameter. Removing the catch param changes semantics.
@@ -753,6 +753,8 @@ fn optional_catch_binding() {
test("try { foo } catch(e) {}", "try { foo } catch {}");
test("try { foo } catch(e) {foo}", "try { foo } catch {foo}");
test_same("try { foo } catch(e) { bar(e) }");
test_same("try { throw 'caught'; } catch (e) { eval('console.log(e)'); }");
test_same("try { throw 'caught'; } catch (e) { function f() { eval('console.log(e)') } f() }");
test_same("try { foo } catch([e]) {}");
test_same("try { foo } catch({e}) {}");
test_same("try { foo } catch(e) { var e = baz; bar(e) }");