Skip to content

Fix parsing of async() calls in conditional expressions - #64234

Open
Mohit Nayak (mohit-nayak) wants to merge 1 commit into
microsoft:mainfrom
mohit-nayak:fix-async-call-in-conditional
Open

Fix parsing of async() calls in conditional expressions#64234
Mohit Nayak (mohit-nayak) wants to merge 1 commit into
microsoft:mainfrom
mohit-nayak:fix-async-call-in-conditional

Conversation

@mohit-nayak

Copy link
Copy Markdown
Contributor

Fixes #64231

Problem

Calling a function named async with no arguments in the true branch of a conditional expression gets parsed as the start of an async arrow function:

declare const a: boolean;
declare function async(): number;

const first = a ? async() : 0; // error TS1005: '=>' expected.

const second = a ? async() : x => x; // error TS1005: ':' expected.

async is not a reserved word, so async() is a plain call and the colon is the conditional's separator. Node runs the JavaScript version of this without complaint, but we reject it in both .ts and .js files. The issue says it fails in every version, and that matches: Strada has the same lookahead.

Cause

nextIsParenthesizedArrowFunctionExpression has a shortcut for empty parameter lists. If it sees "()" followed by "=>", ":" or "{", it returns TSTrue, meaning "this is definitely an arrow function". When the parens come after async, it skips the async and takes the same shortcut. That is not safe, because "async()" followed by a colon can also be a call inside a conditional.

What happens in the repro:

  1. parseConditionalExpressionRest parses the true branch with allowReturnTypeInArrowFunction set to false. That flag exists so "a ? (x) : y" does not read ": y" as a return type.
  2. tryParseParenthesizedArrowFunctionExpression asks the lookahead, which skips async, sees "( ) :", and returns TSTrue.
  3. On TSTrue, the parser calls parseParenthesizedArrowFunctionExpression with allowAmbiguity and allowReturnTypeInArrowFunction both true, which throws away the false passed down from the conditional.
  4. It parses ": 0" as a return type annotation, then expects "=>" and reports TS1005. The guard that handles this case (!allowReturnTypeInArrowFunction && hasReturnColon) never runs, because the flag is now true.

The shortcut is correct for a bare "()", since that can never be an expression on its own. It is only wrong after async, where "async()" is a valid call.

Solution

When the lookahead skipped async and sees "( ) :", return TSUnknown instead of TSTrue. That sends it through the same speculative path "(x):" already uses. It only commits to an arrow if "=>" or "{" follows, it respects the conditional's return type rule, and otherwise it rewinds and parses a call.

Nothing else changes. "()" followed by "=>", ":" or "{" without async is still TSTrue. "async() =>" and "async() {" are still TSTrue, since neither can be a call. "async(): T => x" still parses as an arrow outside a conditional and in either branch of one; "a ? async(): T => x : y" works through the existing second-colon check. The only extra cost is one speculative parse when "async():" shows up, and the existing notParenthesizedArrow cache already memoizes it.

Tests

Added tsc/testdata/tests/cases/compiler/asyncCallInConditionalExpression.ts with three groups of cases:

  • both repros from the issue, plus a call with arguments and a nested conditional, which must now be clean
  • the same repros in a .js file, since the issue says JavaScript fails too
  • real async arrows with and without a return type, with and without a space after async, and in both branches of a conditional, which must still parse as arrows

I ran the test before changing the parser to make sure it catches the bug. It produced 19 errors, with TS1005 on every repro. With the fix there are none, so there is no .errors.txt baseline. The .types baseline shows every async() in a conditional as a call returning number, and every arrow case still comes out as () => Promise.

Across the full suite the only baselines written were the three for this new test, so nothing else moved. npx hereby test:all passed clean. npx hereby test had one failure, TestFSEventsNFDOnDiskNFCSubscribe in internal/fswatch. That package does not depend on the parser and the test passed on 3 of 3 reruns, so it looks like a flake.

Checklist

  • There is an associated issue in the Backlog milestone (required)
    Parser misinterprets async() calls in conditional expressions as async arrow functions #64231 has not been triaged yet, so it has no milestone. I cannot set one. Happy to wait for triage if you would rather look at the issue first.
  • Code is up-to-date with the main branch
  • You've successfully run npx hereby test
  • You've successfully run npx hereby lint
  • You've successfully run npx hereby check:format
  • There are new or updated tests validating the change

Copilot AI balanced review requested due to automatic review settings September 10, 2026 18:25
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Sep 10, 2026
@typescript-automation typescript-automation Bot added For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Sep 10, 2026
@typescript-automation

Copy link
Copy Markdown

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused parser change resolves the reported ambiguity while preserving async-arrow parsing with comprehensive regression baselines.

Pull request overview

Fixes ambiguity between async() calls and async arrow functions in conditional expressions.

Changes:

  • Uses speculative parsing for async(): constructs.
  • Adds TypeScript and JavaScript regression coverage.
  • Adds emit, symbol, and type baselines.
File summaries
File Description
tsc/internal/parser/parser.go Corrects async-call lookahead behavior.
tsc/testdata/tests/cases/compiler/asyncCallInConditionalExpression.ts Adds regression cases.
tsc/testdata/baselines/reference/compiler/asyncCallInConditionalExpression.js Records emitted output.
tsc/testdata/baselines/reference/compiler/asyncCallInConditionalExpression.symbols Records symbol resolution.
tsc/testdata/baselines/reference/compiler/asyncCallInConditionalExpression.types Records inferred types.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@typescript-automation typescript-automation Bot added For Milestone Bug PRs that fix a bug with a specific milestone and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Milestone Bug PRs that fix a bug with a specific milestone

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

Parser misinterprets async() calls in conditional expressions as async arrow functions

2 participants