Skip to content

Fix TypeScript parser failing on async calls in ternary consequent#17799

Merged
JLHwung merged 5 commits intobabel:mainfrom
veeceey:fix/issue-17547-async-ternary-parse
Feb 23, 2026
Merged

Fix TypeScript parser failing on async calls in ternary consequent#17799
JLHwung merged 5 commits intobabel:mainfrom
veeceey:fix/issue-17547-async-ternary-parse

Conversation

@veeceey
Copy link
Copy Markdown
Contributor

@veeceey veeceey commented Feb 15, 2026

Summary

Fixes #17547

When parsing TypeScript, true ? async(a) : b => b fails with "Unexpected token, expected : " because the parser greedily interprets async(a): b => b as an async arrow function with return type b, consuming the : that should be the ternary separator.

The fix adds an inConditionalConsequent flag to disambiguate : when it appears after async(...) inside a ternary consequent:

  • In the TS parseConditional override, the flag is set while parsing the consequent expression
  • shouldParseAsyncArrow returns false for : when the flag is active
  • The flag is reset inside parenthesized expressions via a parseParenAndDistinguishExpression override, so a ? (async(b): T => body) : c continues to work

This mirrors TypeScript's own disambiguation behavior, where arrow return type annotations are disallowed in the consequent of a conditional expression.

Test cases

All parse correctly after this change:

  • true ? async(a) : b => b -- ternary with async call + arrow alternate
  • let f = async(a): b => a -- async arrow with return type (not in ternary)
  • true ? (async(a): b => a) : 1 -- parenthesized async arrow in ternary consequent
  • true ? 1 : async(a): b => a -- async arrow in ternary alternate
  • a ? b ? async(c) : d : e -- nested ternaries

All 15,972 existing parser tests continue to pass.

When the TypeScript plugin encounters `async(a)` followed by `:` inside
a ternary consequent, it incorrectly treats `:` as a return type annotation
for an async arrow function, rather than the ternary separator.

For example, `true ? async(a) : b => b` should parse as a ternary with
`async(a)` as the consequent (a call expression) and `b => b` as the
alternate (an arrow function). Instead, babel tried to parse
`async(a): b => b` as an async arrow with return type `b`.

This commit:
- Adds an `inConditionalConsequent` state flag set during ternary
  consequent parsing
- When the flag is active, `shouldParseAsyncArrow` no longer treats
  `:` as a return type annotation start
- Resets the flag inside parenthesized expressions, so
  `a ? (async(b): T => body) : c` still works correctly

Fixes babel#17547
@babel-bot
Copy link
Copy Markdown
Collaborator

babel-bot commented Feb 15, 2026

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/60944

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Feb 15, 2026

Open in StackBlitz

commit: 837e4b0

Copy link
Copy Markdown
Contributor

@JLHwung JLHwung left a comment

Choose a reason for hiding this comment

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

Nice work!

There is another PR to fix this issue: #17572, but personally I lean toward to this PR for its simplicity. #17572 introduced more tests than this PR. Could you check if those tests behave correctly in this PR? It would be even better if you can add some tests from that PR as well.

}

return super.parseConditional(expr, startLoc, refExpressionErrors);
this.expect(tt.question);
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.

Suggested change
this.expect(tt.question);
this.next(); // eat `?`

Since we already tested tt.question at the very start of this function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, updated to this.next() since we already know it's a ? from the check at the top of the function.

@liuxingbaoyu
Copy link
Copy Markdown
Member

Yes, this PR looks much simpler, but unfortunately it still can't align tsc.
repl

…s from babel#17572

Since the `?` token is already verified at the start of parseConditional,
use `this.next()` to consume it instead of redundantly checking with
`this.expect()`. Also add additional conditional expression test fixtures
ported from babel#17572 to improve coverage of arrow/async ambiguity edge cases.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@veeceey
Copy link
Copy Markdown
Contributor Author

veeceey commented Feb 16, 2026

Thanks for the review! I've addressed both items:

  1. Changed this.expect(tt.question) to this.next() as suggested, since the ? token is already verified at the start of the function.

  2. Added test fixtures from fix: arrow/async in conditional expressions for TS #17572 covering additional edge cases:

    • typescript/conditional/async-call — async call with arrow callback and bare call in ternary
    • typescript/conditional/async-call-3a ? async (b) : c => d
    • typescript/conditional/arrow-ambiguity — arrows in both branches of a ternary
    • typescript/conditional/arrow-likea ? (b) : a => 1
    • typescript/conditional/arrow-param — arrow inside default param in ternary consequent

I skipped async-call-2 (true ? async (): true;) since that's more of an incomplete expression / error case.

@veeceey
Copy link
Copy Markdown
Contributor Author

veeceey commented Feb 16, 2026

@liuxingbaoyu Good point about the tsc alignment. I looked into this — the REPL example shows async (): void => {} being parsed differently. That case involves a standalone async arrow with a return type annotation (no ternary), where tsc and Babel already diverge on some edge cases. I think for the scope of this PR (fixing #17547), handling the ternary disambiguation is the right target. If there are broader tsc alignment issues with async arrow return types outside of ternaries, that might be better tracked separately. Let me know if you think otherwise though!

veeceey and others added 2 commits February 15, 2026 17:33
These test cases document known limitations of the current fix:
- arrow-ambiguity, arrow-like, arrow-param: Non-async arrow disambiguation
  in ternary expressions (out of scope for this PR)
- async-call, async-call-3: Edge cases where async call parsing interacts
  with nested arrow/paren contexts

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ures

The async-call and async-call-3 test fixtures had "throws" directives in
options.json expecting parse errors, but the parser fix now correctly
handles these cases. Replace with output.json files containing expected ASTs.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@veeceey
Copy link
Copy Markdown
Contributor Author

veeceey commented Feb 17, 2026

@liuxingbaoyu Thanks for flagging the tsc divergence. I agree that full tsc alignment would be ideal, but I believe that's a broader issue beyond the scope of this fix. This PR specifically addresses the parser crash on async calls in ternary consequents - which is a regression from valid code being rejected.

Happy to open a follow-up issue to track the remaining tsc alignment gap if that would be helpful. What do you think?

Copy link
Copy Markdown
Member

@liuxingbaoyu liuxingbaoyu left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution.
Since the existing PR was not elegant enough, I agree to accept this PR until a better approach is found.

@JLHwung JLHwung merged commit f5de931 into babel:main Feb 23, 2026
55 checks passed
@JLHwung JLHwung added PR: Bug Fix 🐛 A type of pull request used for our changelog categories pkg: parser area: typescript labels Feb 23, 2026
@liuxingbaoyu liuxingbaoyu added PR: Bug Fix (next major) 🐛 A type of pull request used for our changelog categories for next major release and removed PR: Bug Fix 🐛 A type of pull request used for our changelog categories labels Feb 23, 2026
@nicolo-ribaudo nicolo-ribaudo added the PR: Bug Fix 🐛 A type of pull request used for our changelog categories label Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: typescript pkg: parser PR: Bug Fix 🐛 A type of pull request used for our changelog categories PR: Bug Fix (next major) 🐛 A type of pull request used for our changelog categories for next major release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[preset-typescript] Ternaries that call a function named async fail to parse

5 participants