Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
evanw opened this issue Sep 7, 2020 · 2 comments · Fixed by #56296
Closed

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

evanw opened this issue Sep 7, 2020 · 2 comments · Fixed by #56296
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@evanw
Copy link
Contributor

evanw commented Sep 7, 2020

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:

@SalathielGenese
Copy link

The playground example you provided does not feature what you are describing.

@evanw
Copy link
Contributor Author

evanw commented Sep 7, 2020

If you open the playground link and click Run, you will get this error in the developer console (in Chrome, other browsers will have slightly different errors):

Uncaught (in promise) ReferenceError: z is not defined
    at f1 (eval at <anonymous> (runtime.ts:68), <anonymous>:12:24)
    at eval (eval at <anonymous> (runtime.ts:68), <anonymous>:19:17)
    at Generator.next (<anonymous>)
    at eval (eval at <anonymous> (runtime.ts:68), <anonymous>:8:71)
    at new Promise (<anonymous>)
    at __awaiter (eval at <anonymous> (runtime.ts:68), <anonymous>:4:12)
    at eval (eval at <anonymous> (runtime.ts:68), <anonymous>:11:8)
    at eval (eval at <anonymous> (runtime.ts:68), <anonymous>:28:4)
    at runtime.ts:68

If you change the target from ES2015 to ESNext and click run, there will be no errors printed to the developer console.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants