Skip to content

feat(Async): Add exception-unwrapping Await - #19785

Merged
T-Gro merged 40 commits into
dotnet:mainfrom
bartelink:async-await
Aug 13, 2026
Merged

feat(Async): Add exception-unwrapping Await#19785
T-Gro merged 40 commits into
dotnet:mainfrom
bartelink:async-await

Conversation

@bartelink

@bartelink bartelink commented May 21, 2026

Copy link
Copy Markdown
Contributor

Implements Async.Await for Task, Task<'T>, ValueTask and ValueTask<'T> (aka a polished version of the
community AwaitTaskCorrect implementation). Includes a fallback SRTP based type augmentation that uses the GetAwaiter protocol to support custom waits a la C# await.

Key differentiation from Async.AwaitTask is that AggregateExceptions are unwrapped such that a try ... with <ExceptionType> -> will type-match correctly.

Key distinction from the canonical implementation (which derives from https://www.fssnip.net/7Rc/title/AsyncAwaitTaskCorrect) is that the implementation is intended to have 1:1 matching of all stacktrace preservation properties borne by AwaitTask (and continue to track that over time).

NOTE one key implementation decision is that this PR does NOT attempt to resolve #2127 so:

  1. if the computation workflow's CT is cancelled at the point where Await is invoked, normal cancellation semantics as per AwaitTask apply:
    • exception continuation is passed an OperationCanceledException
    • the Task in question's Result will go unobserved
  2. if a cancellation of the computation workflow via it's ambient cancellation token is triggered during the course of the Await, it will (like AwaitTask):
    • NOT abort and abandon the observation Task
    • instead, it will wait [as a C# await would] until such time as the Task completes (either successfully, with a fault, or via cancellation)

Updates the FSharpPlus regression test commit reference to include fsprojects/FSharpPlus#677, which aligns the semantics with those here and in the canonical fssnip base implementation.

Checklist

  • Test cases added
  • Release notes entry updated
  • Add documentation cross-links and strongly-implied deprecation flagging conveying that:
    • AwaitTask will always yield an AggregateException
    • Await should be the used in preference for new code
    • Await can technically still propagate an AggregateException

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Warning

No PR link found in some release notes, please consider adding it.

Change path Release notes path Description
`src/FSharp.Core` docs/release-notes/.FSharp.Core/11.0.100.md
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.100.md No current pull request URL (#19785) found, please consider adding it

@bartelink
bartelink force-pushed the async-await branch 7 times, most recently from b2b2b6a to b710cc1 Compare May 21, 2026 14:58
@bartelink

bartelink commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

@T-Gro if you and/or others can give this a quick scan please, I'd like to confirm:

  1. nobody else has work in flight (yes, a bit late for that!)
  2. the rough approach is viable
  3. the key impl decision is reasonable:
    • no attempt to get too clever and have the API react to Async cancellation by abandoning waits, i.e. if you have code that does not propagate Async.CancellationToken to a Task start, Await will hang just as AwaitTask
      • BUT this is OK as abandoning in-flight tasks and/or the controlled disposal of associated resources/compute would not be strictly better
      • It aligns with e.g. how Async.Parallel waits for correct teardown/completion of all in-flight executions before yielding a result and/or completing the honoring of cancellation
    • Best practice recommendation will instead be to use Async.StartTaskImmediate, which will
  4. a rough indication of whether I should leave the xmldoc as is, or attempt to complete the rough tasks I've laid out in the checklist in the OP

TL;DR the overall proposition

  1. Await is just AwaitTask with unwrapping, zero other semantic change. => A better default to use where you'd otherwise use AwaitTask
  2. Usage of AwaitTask and Await are both smells - can you be sure all Tasks that have been started were correctly wired into the computation tree's CT?
  3. In general, usage of Async.Await should be replaced with/migrated to Async.StartTaskImmediate, which surfaces the problem
  4. if there was an analyzer flagging Async.Await/AwaitTask -> Async.StartTaskImmediate migration opportunities, a closely related one would be flagging cases of task { flows that use let! and/or do! bindings against Async<'T>) rather than using Task.startAsyncImmediate (which forces passing a CT to Async.StartImmediateAsTask

CC @TheAngryByrd @gusty @njlr who have provided useful feedback/review on stuff in this space in recent times

@bartelink
bartelink marked this pull request as ready for review May 22, 2026 13:37
Copilot AI review requested due to automatic review settings May 22, 2026 13:37
@bartelink
bartelink requested a review from a team as a code owner May 22, 2026 13:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Async.Await API to FSharp.Core for Task/Task<'T> (and ValueTask/ValueTask<'T> on netstandard2.1) that unwraps “egregious” single-inner AggregateExceptions so exception matching works as expected, while aiming to preserve existing AwaitTask stacktrace behavior.

Changes:

  • Implement Async.Await in async.fs by sharing the existing AwaitTask continuation machinery and selectively unwrapping single-inner AggregateExceptions.
  • Expand unit tests to exercise both AwaitTask and Await behavior (including AggregateException cases) and add ValueTask coverage under #if NETSTANDARD2_1.
  • Update FSharp.Core surface area baseline (partially) and add a release-notes entry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs Converts many tests to run against both AwaitTask and new Await; adds new behavior-focused tests for AggregateException unwrapping and ValueTask.
tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl Adds Async.Await entries for netstandard2.1 Debug surface area baseline.
src/FSharp.Core/async.fsi Updates AwaitTask XML docs and adds new Async.Await API docs (including ValueTask overloads under netstandard2.1).
src/FSharp.Core/async.fs Implements Async.Await and refactors task-await internals to optionally unwrap single-inner AggregateExceptions.
docs/release-notes/.FSharp.Core/11.0.100.md Adds release note entry for new Async.Await.

Comment thread src/FSharp.Core/async.fsi
Comment thread src/FSharp.Core/async.fs Outdated
@bartelink
bartelink force-pushed the async-await branch 2 times, most recently from 7aec92d to 9ab202c Compare May 22, 2026 14:03
@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label May 22, 2026
Comment thread src/FSharp.Core/async.fs Outdated
Comment thread src/FSharp.Core/async.fs Outdated
@majocha

majocha commented May 22, 2026

Copy link
Copy Markdown
Contributor

I'm thinking about naming. Async.Await name does not suggest that it applies to Tasks only. Since it is already 4 times overloaded, maybe it makes sense to add an overload working with any awaitable, to have parity with C#? This can be discussed and done separately from this PR, of course. (Also SRTP can be a can of worms).

@bartelink

Copy link
Copy Markdown
Contributor Author

maybe it makes sense to add an overload working with any awaitable, to have parity with C#? This can be discussed and done separately from this PR, of course. (Also SRTP can be a can of worms).

@majocha Yes, this was already part of the brief as per the comment from @dsyme.

I'd personally need to research what it would entail, so if you or anyone wants to contribute an impl, feel free to hang a PR off this one.

But bottom line I agree it would be good for the support for tasklike things to be done either as part of this PR or as a very fast follow so the world only needs to validate the overloading works out cleanly once.

@bartelink
bartelink force-pushed the async-await branch 2 times, most recently from a887f40 to 885c40f Compare May 23, 2026 11:11
@bartelink

Copy link
Copy Markdown
Contributor Author

I'd personally need to research what it would entail, so if you or anyone wants to contribute an impl, feel free to hang a PR off this one.

. @majocha Added a commit:

  • as demonstrated by the test, the GetResult() already unwraps, so it would seem to me that doing deeper integration with the internals of the specific-type overloads won't have any benefit
  • open question is whether all 4 overloads of the existing implementations are still earning their keep in terms of:
    • perf and allocations (Benchmark tests are probably the real answer to that)
    • extending platform reach (does the GetAwaiter protocol work for downlevel TFMs?)

@bartelink
bartelink force-pushed the async-await branch 2 times, most recently from 93badc9 to 50814f5 Compare May 25, 2026 12:03
@github-actions github-actions Bot added ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure labels Aug 10, 2026
@github-actions

This comment has been minimized.

@bartelink

bartelink commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@T-Gro updated to target your branch and merged fsharp/main from earlier today
Your Await changes seem to have hit the mark but now we have a failing test in the F#+ memoization module (same fail on net10, net11)

Also @wallymathieu @gusty there's another new compiler interaction with F#+ source:

D:\a\_work\1\TestRepo\src\FSharpPlus.Docs\Samples\Collections.fsx(80,1): error FS3875: Warn directives must have warning number(s) as argument(s) [D:\a\_work\1\TestRepo\src\FSharpPlus.Docs\FSharpPlus.Docs.fsproj]

@gusty

gusty commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@bartelink Yes, I need to change those #nowarn to ;;that's it.

@bartelink

bartelink commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@T-Gro Update:- your PR got merged into FSharpPlus; I've repointed this at the post-merge commit on master

T-Gro and others added 4 commits August 12, 2026 10:56
The function-domain unification order change in PR dotnet#15181 (nullness) caused
recursive inline SRTP resolution to be truncated by one currying level: an
inference variable still carrying an unsolved member constraint could be
absorbed by the required domain, merging away the pending recursive trait
resolution. This regressed patterns such as FSharpPlus `memoizeN`.

SolveFunTypeEqn now keeps the SRTP-constrained inference variable as the
unification representative for that specific case, and only when not
MatchingOnly (mirroring SolveTypeEqualsType).

Adds two ComponentTests (the memoizeN repro across currying depths, and a
MatchingOnly overload-resolution guard) and repoints the FSharpPlus
regression matrix at a minimal global.json-bump branch that exercises the
fix end-to-end.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4cfb1c86-0d7e-45e6-9529-3c5f301c0912
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4cfb1c86-0d7e-45e6-9529-3c5f301c0912
@bartelink

bartelink commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #20247


@T-Gro confirming the remaining blocker is the fact that the FSharpPlus regression build has a a failing test in its memoization module. The project itself is green at the same commit hash using an 8.0 sdk.

Thus analysis is required to determine whether the memoization was working by coincidence in that context, or whether the updated compiler/toolset is to blame/broken.

I see there is/was a .NET 10 branch that commented out the assertion as the same issue was observed - the simplest 🙈 workaround would be to make a PR against FSharpPlus with that one liner commenting out of the assertion and then pin to that.

If you have any ideas on a path to resolving the issue, I may be able to pitch in, but for now it's surrounded by a large SEP field my side :P

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Build-Infra, Affects-Compiler-Output
Affects-Build-Infra: modifies azure-pipelines-PR.yml (CI pipeline)
Affects-Compiler-Output: modifies ConstraintSolver.fs

Generated by PR Tooling Safety Check · opus46 3.3M ·

@bartelink

bartelink commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@T-Gro Nice work resolving the memoizeN issue

This is green now (with #20427 merged into here temporarily) and all FSharpPlus regressions pointed at your f42f81885111c652b08218e0880c264447ae56e4 temp commit in FSharpPlus

After this merges, I intend to proceed with bartelink#1

@T-Gro
T-Gro merged commit 1e94cc5 into dotnet:main Aug 13, 2026
49 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in F# Compiler and Tooling Aug 13, 2026
@T-Gro

T-Gro commented Aug 13, 2026

Copy link
Copy Markdown
Member

Congrats for pushing trough all the obstacles encountered on the road 🥇 .

The F#+ pin should now remain to this SHA. And once the compiler change lands, we can also open the same PR to F#+/master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Async.AwaitTask does not cancel on workflow cancellation

6 participants