Skip to content

Behavior change when lowering async function which throws during argument evaluation #40410

Closed
@evanw

Description

@evanw

TypeScript Version: 4.0.2

Search Terms: async argument throw emit lower incorrect behavior

Code

(async() => {
  async function f1(x, y = z) {}
  async function f2({[z]: x}) {}
  for (let f of [f1, f2]) {
    let p = f({}) // This should not throw an error
    try {
      await p
    } catch (e) {
      continue // This error is expected
    }
    throw new Error('Expected an error')
  }
})()

Expected behavior:

The code should run without crashing, both when compiled to ESNext and when compiled to ES2015.

Actual behavior:

The code crashes with ReferenceError: z is not defined when compiled to ES2015. This is because the generated code moves the evaluation of default arguments from inside the promise context to outside of the promise context during lowering:

function f1(x, y = z) {
  return __awaiter(this, void 0, void 0, function* () { });
}
function f2({ [z]: x }) {
  return __awaiter(this, void 0, void 0, function* () { });
}

There are many ways to fix this, but an example correct compilation might look something like this:

function f1(_0) {
  return __awaiter(this, arguments, void 0, function* (x, y = z) { });
}
function f2(_0) {
  return __awaiter(this, arguments, void 0, function* ({ [z]: x }) { });
}

The _0 variables are to ensure f1.length and f2.length don't change.

I discovered this correctness issue because I replicated TypeScript's approach to async function lowering into esbuild but it turned out to have this correctness issue.

Playground Link: link

Related Issues:

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issueRescheduledThis issue was previously scheduled to an earlier milestone

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions