From 0677d343aff10c3ac0755a47c40b1c8e7928d290 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Thu, 21 May 2026 14:20:27 +0100
Subject: [PATCH 01/26] feat(Async): Add exception-unwrapping Await
---
docs/release-notes/.FSharp.Core/11.0.100.md | 4 +
src/FSharp.Core/async.fs | 94 +++--
src/FSharp.Core/async.fsi | 87 ++++-
...p.Core.SurfaceArea.netstandard20.debug.bsl | 2 +
...Core.SurfaceArea.netstandard20.release.bsl | 2 +
...p.Core.SurfaceArea.netstandard21.debug.bsl | 4 +
...Core.SurfaceArea.netstandard21.release.bsl | 4 +
.../Microsoft.FSharp.Control/AsyncType.fs | 343 +++++++++++++-----
8 files changed, 420 insertions(+), 120 deletions(-)
diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md
index 7d0385e3b89..fe933aca0fd 100644
--- a/docs/release-notes/.FSharp.Core/11.0.100.md
+++ b/docs/release-notes/.FSharp.Core/11.0.100.md
@@ -3,3 +3,7 @@
* Fix `Array.exists2` documentation examples to use equal-length arrays; the previous examples would throw `ArgumentException` at runtime instead of returning the documented `false`/`true` values. ([PR #19672](https://github.com/dotnet/fsharp/pull/19672))
* Move `Async.StartChild` to the "Starting Async Computations" docs category alongside `Async.StartChildAsTask`. ([Issue #19667](https://github.com/dotnet/fsharp/issues/19667))
* Add `InlineIfLambda` to `Array.init` ([PR #19869](https://github.com/dotnet/fsharp/pull/19869))
+
+### Added
+
+* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785))
diff --git a/src/FSharp.Core/async.fs b/src/FSharp.Core/async.fs
index f18e451f357..ca8197490b5 100644
--- a/src/FSharp.Core/async.fs
+++ b/src/FSharp.Core/async.fs
@@ -1210,16 +1210,30 @@ module AsyncPrimitives =
task
+ // Used by Async.Await path to elide egregious AggregateException wrapping
+ []
+ let UnwrapExn (exn: AggregateException) =
+ if exn.InnerExceptions.Count = 1 then
+ exn.InnerExceptions[0]
+ else
+ exn
+
// Call the appropriate continuation on completion of a task
[]
- let OnTaskCompleted (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) =
+ let OnTaskCompleted unwrap (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) =
assert completedTask.IsCompleted
if completedTask.IsCanceled then
let edi = ExceptionDispatchInfo.Capture(TaskCanceledException completedTask)
ctxt.econt edi
elif completedTask.IsFaulted then
- let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception
+ let e =
+ if unwrap then
+ UnwrapExn completedTask.Exception
+ else
+ completedTask.Exception
+
+ let edi = ExceptionDispatchInfo.RestoreOrCapture e
ctxt.econt edi
else
ctxt.cont completedTask.Result
@@ -1229,14 +1243,20 @@ module AsyncPrimitives =
// the overall async (they may be governed by different cancellation tokens, or
// the task may not have a cancellation token at all).
[]
- let OnUnitTaskCompleted (completedTask: Task) (ctxt: AsyncActivation) =
+ let OnUnitTaskCompleted unwrap (completedTask: Task) (ctxt: AsyncActivation) =
assert completedTask.IsCompleted
if completedTask.IsCanceled then
let edi = ExceptionDispatchInfo.Capture(TaskCanceledException(completedTask))
ctxt.econt edi
elif completedTask.IsFaulted then
- let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception
+ let e =
+ if unwrap then
+ UnwrapExn completedTask.Exception
+ else
+ completedTask.Exception
+
+ let edi = ExceptionDispatchInfo.RestoreOrCapture e
ctxt.econt edi
else
ctxt.cont ()
@@ -1246,10 +1266,10 @@ module AsyncPrimitives =
// completing the task. This will install a new trampoline on that thread and continue the
// execution of the async there.
[]
- let AttachContinuationToTask (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
+ let AttachContinuationToTask unwrap (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
task.ContinueWith(
Action>(fun completedTask ->
- ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted completedTask ctxt)
+ ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted unwrap completedTask ctxt)
|> unfake),
TaskContinuationOptions.ExecuteSynchronously
)
@@ -1261,16 +1281,36 @@ module AsyncPrimitives =
// completing the task. This will install a new trampoline on that thread and continue the
// execution of the async there.
[]
- let AttachContinuationToUnitTask (task: Task) (ctxt: AsyncActivation) =
+ let AttachContinuationToUnitTask unwrap (task: Task) (ctxt: AsyncActivation) =
task.ContinueWith(
Action(fun completedTask ->
- ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnUnitTaskCompleted completedTask ctxt)
+ ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnUnitTaskCompleted unwrap completedTask ctxt)
|> unfake),
TaskContinuationOptions.ExecuteSynchronously
)
|> ignore
|> fake
+ let AwaitTask unwrap (task: Task<'T>) =
+ MakeAsyncWithCancelCheck(fun ctxt ->
+ if task.IsCompleted then
+ // Run synchronously without installing new trampoline
+ OnTaskCompleted unwrap task ctxt
+ else
+ // Continue asynchronously, via syncContext if necessary, installing new trampoline
+ let ctxt = DelimitSyncContext ctxt
+ ctxt.ProtectCode(fun () -> AttachContinuationToTask unwrap task ctxt))
+
+ let AwaitUnitTask unwrap (task: Task) =
+ MakeAsyncWithCancelCheck(fun ctxt ->
+ if task.IsCompleted then
+ // Continue synchronously without installing new trampoline
+ OnUnitTaskCompleted unwrap task ctxt
+ else
+ // Continue asynchronously, via syncContext if necessary, installing new trampoline
+ let ctxt = DelimitSyncContext ctxt
+ ctxt.ProtectCode(fun () -> AttachContinuationToUnitTask unwrap task ctxt))
+
/// Removes a registration places on a cancellation token
let DisposeCancellationRegistration (registration: byref) =
match registration with
@@ -2203,24 +2243,30 @@ type Async =
CreateWhenCancelledAsync compensation computation
static member AwaitTask(task: Task<'T>) : Async<'T> =
- MakeAsyncWithCancelCheck(fun ctxt ->
- if task.IsCompleted then
- // Run synchronously without installing new trampoline
- OnTaskCompleted task ctxt
- else
- // Continue asynchronously, via syncContext if necessary, installing new trampoline
- let ctxt = DelimitSyncContext ctxt
- ctxt.ProtectCode(fun () -> AttachContinuationToTask task ctxt))
+ AwaitTask false task
static member AwaitTask(task: Task) : Async =
- MakeAsyncWithCancelCheck(fun ctxt ->
- if task.IsCompleted then
- // Continue synchronously without installing new trampoline
- OnUnitTaskCompleted task ctxt
- else
- // Continue asynchronously, via syncContext if necessary, installing new trampoline
- let ctxt = DelimitSyncContext ctxt
- ctxt.ProtectCode(fun () -> AttachContinuationToUnitTask task ctxt))
+ AwaitUnitTask false task
+
+ static member Await(task: Task<'T>) : Async<'T> =
+ AwaitTask true task
+
+ static member Await(task: Task) : Async =
+ AwaitUnitTask true task
+
+#if NETSTANDARD2_1
+ static member Await(task: ValueTask<'T>) : Async<'T> =
+ if task.IsCompleted then
+ async { return task.GetAwaiter().GetResult() }
+ else
+ Async.Await(task.AsTask())
+
+ static member Await(task: ValueTask) : Async =
+ if task.IsCompleted then
+ async { return task.GetAwaiter().GetResult() }
+ else
+ Async.Await(task.AsTask())
+#endif
module CommonExtensions =
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index b2fe66ddd13..5a5a991c293 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -740,19 +740,18 @@ namespace Microsoft.FSharp.Control
///
static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout:int -> Async
- /// Return an asynchronous computation that will wait for the given task to complete and return
+ /// Creates an asynchronous computation that will wait asynchronously for the given task to complete, returning
/// its result.
///
/// The task to await.
///
- /// If an exception occurs in the asynchronous computation then an exception is re-raised by this
- /// function.
+ /// If the task yields an exception, then then the full underlying is re-raised by this function.
///
- /// If the task is cancelled then is raised. Note
+ /// If the task is canceled then is raised. Note
/// that the task may be governed by a different cancellation token to the overall async computation
/// where the AwaitTask occurs. In practice you should normally start the task with the
/// cancellation token returned by let! ct = Async.CancellationToken, and catch
- /// any at the point where the
+ /// any at the point where the
/// overall async is started.
///
///
@@ -761,19 +760,17 @@ namespace Microsoft.FSharp.Control
///
static member AwaitTask: task: Task<'T> -> Async<'T>
- /// Return an asynchronous computation that will wait for the given task to complete and return
- /// its result.
+ /// Creates an asynchronous computation that will wait asynchronously for the given task to complete.
///
/// The task to await.
///
- /// If an exception occurs in the asynchronous computation then an exception is re-raised by this
- /// function.
+ /// If the task yields an exception, then the full underlying is re-raised by this function.
///
- /// If the task is cancelled then is raised. Note
+ /// If the task is canceled then is raised. Note
/// that the task may be governed by a different cancellation token to the overall async computation
/// where the AwaitTask occurs. In practice you should normally start the task with the
/// cancellation token returned by let! ct = Async.CancellationToken, and catch
- /// any at the point where the
+ /// any at the point where the
/// overall async is started.
///
///
@@ -782,6 +779,74 @@ namespace Microsoft.FSharp.Control
///
static member AwaitTask: task: Task -> Async
+ /// Creates an asynchronous computation that will wait for the given task to complete and return
+ /// its result.
+ ///
+ /// The task to await.
+ ///
+ /// Exceptions from the task are surfaced directly, without wrapping in
+ /// . An
+ /// will only be surfaced where multiple inner exceptions are present.
+ ///
+ /// If the task is canceled then is raised.
+ ///
+ ///
+ /// Awaiting Results
+ ///
+ ///
+ static member Await: task: Task<'T> -> Async<'T>
+
+ /// Creates an asynchronous computation that will wait for the given task to complete.
+ ///
+ /// The task to await.
+ ///
+ /// Exceptions from the task are surfaced directly, without wrapping in
+ /// . An
+ /// will only be surfaced where multiple inner exceptions are present.
+ ///
+ /// If the task is canceled then is raised.
+ ///
+ ///
+ /// Awaiting Results
+ ///
+ ///
+ static member Await: task: Task -> Async
+
+#if NETSTANDARD2_1
+ /// Return an asynchronous computation that will wait for the given task to complete and return
+ /// its result.
+ ///
+ /// The ValueTask to await.
+ ///
+ /// Exceptions from the task are surfaced directly, without wrapping in
+ /// . An
+ /// will only be surfaced where multiple inner exceptions are present.
+ ///
+ /// If the task is canceled then is raised.
+ ///
+ ///
+ /// Awaiting Results
+ ///
+ ///
+ static member Await: task: ValueTask<'T> -> Async<'T>
+
+ /// Return an asynchronous computation that will wait for the given ValueTask to complete.
+ ///
+ /// The task to await.
+ ///
+ /// Exceptions from the task are surfaced directly, without wrapping in
+ /// . An
+ /// will only be surfaced where multiple inner exceptions are present.
+ ///
+ /// If the task is canceled then is raised.
+ ///
+ ///
+ /// Awaiting Results
+ ///
+ ///
+ static member Await: task: ValueTask -> Async
+#endif
+
///
/// Creates an asynchronous computation that will sleep for the given time. This is scheduled
/// using a System.Threading.Timer object. The operation will not block operating system threads
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl
index 5b6cc0bce4e..2b534cc247d 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl
@@ -642,6 +642,7 @@ Microsoft.FSharp.Control.EventModule: Void Add[T,TDel](Microsoft.FSharp.Core.FSh
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Control.FSharpAsync`1[T]] StartChild[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpChoice`2[T,System.Exception]] Catch[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[T]] Choice[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[T]]])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Await(System.Threading.Tasks.Task)
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AwaitTask(System.Threading.Tasks.Task)
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(Int32)
@@ -659,6 +660,7 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Parallel[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Sequential[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitEvent[TDel,T](Microsoft.FSharp.Control.IEvent`2[TDel,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.Task`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitTask[T](System.Threading.Tasks.Task`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,TArg3,T](TArg1, TArg2, TArg3, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`5[TArg1,TArg2,TArg3,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,T](TArg1, TArg2, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`4[TArg1,TArg2,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl
index 217d4b7c837..d2682172d43 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl
@@ -642,6 +642,7 @@ Microsoft.FSharp.Control.EventModule: Void Add[T,TDel](Microsoft.FSharp.Core.FSh
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Control.FSharpAsync`1[T]] StartChild[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpChoice`2[T,System.Exception]] Catch[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[T]] Choice[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[T]]])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Await(System.Threading.Tasks.Task)
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AwaitTask(System.Threading.Tasks.Task)
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(Int32)
@@ -659,6 +660,7 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Parallel[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Sequential[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitEvent[TDel,T](Microsoft.FSharp.Control.IEvent`2[TDel,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.Task`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitTask[T](System.Threading.Tasks.Task`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,TArg3,T](TArg1, TArg2, TArg3, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`5[TArg1,TArg2,TArg3,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,T](TArg1, TArg2, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`4[TArg1,TArg2,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl
index 43defdb622e..ab8571b1442 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl
@@ -644,6 +644,8 @@ Microsoft.FSharp.Control.EventModule: Void Add[T,TDel](Microsoft.FSharp.Core.FSh
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Control.FSharpAsync`1[T]] StartChild[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpChoice`2[T,System.Exception]] Catch[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[T]] Choice[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[T]]])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Await(System.Threading.Tasks.Task)
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Await(System.Threading.Tasks.ValueTask)
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AwaitTask(System.Threading.Tasks.Task)
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(Int32)
@@ -661,6 +663,8 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Parallel[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Sequential[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitEvent[TDel,T](Microsoft.FSharp.Control.IEvent`2[TDel,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.Task`1[T])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.ValueTask`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitTask[T](System.Threading.Tasks.Task`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,TArg3,T](TArg1, TArg2, TArg3, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`5[TArg1,TArg2,TArg3,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,T](TArg1, TArg2, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`4[TArg1,TArg2,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl
index ed913ea04d3..69fc090f3fc 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl
@@ -644,6 +644,8 @@ Microsoft.FSharp.Control.EventModule: Void Add[T,TDel](Microsoft.FSharp.Core.FSh
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Control.FSharpAsync`1[T]] StartChild[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpChoice`2[T,System.Exception]] Catch[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[T]] Choice[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[T]]])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Await(System.Threading.Tasks.Task)
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Await(System.Threading.Tasks.ValueTask)
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AwaitTask(System.Threading.Tasks.Task)
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(Int32)
@@ -661,6 +663,8 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Parallel[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Sequential[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitEvent[TDel,T](Microsoft.FSharp.Control.IEvent`2[TDel,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.Task`1[T])
+Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.ValueTask`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitTask[T](System.Threading.Tasks.Task`1[T])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,TArg3,T](TArg1, TArg2, TArg3, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`5[TArg1,TArg2,TArg3,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,T](TArg1, TArg2, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`4[TArg1,TArg2,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]])
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
index 571a4250175..9aea7f42a62 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
@@ -220,14 +220,14 @@ type AsyncType() =
| _ -> reraise()
Assert.True (tcs.Task.IsCompleted, "Task is not completed")
- []
- member _.RunSynchronouslyCancellationWithDelayedResult () =
+ []
+ member _.RunSynchronouslyCancellationWithDelayedResult(newAwait: bool) =
let cts = new CancellationTokenSource()
let tcs = TaskCompletionSource()
let _ = cts.Token.Register(fun () -> tcs.SetResult 42)
let a = async {
- cts.CancelAfter (100)
- let! result = tcs.Task |> Async.AwaitTask
+ cts.CancelAfter(100)
+ let! result = tcs.Task |> if newAwait then Async.Await else Async.AwaitTask
return result }
let cancelled =
@@ -367,127 +367,127 @@ type AsyncType() =
Assert.True(t.IsCanceled)
Assert.True(cancelled)
- []
- member _.TaskAsyncValue () =
+ []
+ member _.TaskAsyncValue(newAwait: bool) =
let s = "Test"
use t = Task.Factory.StartNew(Func<_>(fun () -> s))
let a = async {
- let! s1 = Async.AwaitTask(t)
- return s = s1
- }
- Async.RunSynchronously(a) |> Assert.True
+ let! s1 = t |> if newAwait then Async.Await else Async.AwaitTask
+ return s = s1
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
- []
- member _.AwaitTaskCancellation () =
- let test() = async {
- let tcs = new System.Threading.Tasks.TaskCompletionSource()
+ []
+ member _.AwaitTaskCancellation(newAwait: bool) =
+ let a = async {
+ let tcs = System.Threading.Tasks.TaskCompletionSource()
tcs.SetCanceled()
try
- do! Async.AwaitTask tcs.Task
+ do! tcs.Task |> if newAwait then Async.Await else Async.AwaitTask
return false
- with :? System.OperationCanceledException -> return true
+ with :? OperationCanceledException -> return true
}
-
- Async.RunSynchronously(test()) |> Assert.True
+ let ok = Async.RunSynchronously a
+ Assert.True ok
[]
member _.AwaitCompletedTask() =
- let test() = async {
+ let a = async {
let threadIdBefore = Thread.CurrentThread.ManagedThreadId
do! Async.AwaitTask Task.CompletedTask
let threadIdAfter = Thread.CurrentThread.ManagedThreadId
return threadIdBefore = threadIdAfter
}
+ let ok = Async.RunSynchronously a
+ Assert.True ok
- Async.RunSynchronously(test()) |> Assert.True
-
- []
- member _.AwaitTaskCancellationUntyped () =
- let test() = async {
- let tcs = new System.Threading.Tasks.TaskCompletionSource()
+ []
+ member _.AwaitTaskCancellationUntyped(newAwait: bool) =
+ let a = async {
+ let tcs = System.Threading.Tasks.TaskCompletionSource()
tcs.SetCanceled()
try
- do! Async.AwaitTask (tcs.Task :> Task)
+ do! tcs.Task :> Task |> if newAwait then Async.Await else Async.AwaitTask
return false
- with :? System.OperationCanceledException -> return true
+ with :? OperationCanceledException -> return true
}
+ let ok = Async.RunSynchronously a
+ Assert.True ok
- Async.RunSynchronously(test()) |> Assert.True
-
- []
- member _.TaskAsyncValueException () =
+ []
+ member _.TaskAsyncValueException(newAwait: bool) =
use t = Task.Factory.StartNew(Func(fun () -> raise <| Exception()))
let a = async {
- try
- let! v = Async.AwaitTask(t)
- return false
- with e -> return true
- }
- Async.RunSynchronously(a) |> Assert.True
+ try let! v = t |> if newAwait then Async.Await else Async.AwaitTask
+ return false
+ with e -> return true
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
- []
- member _.TaskAsyncValueCancellation () =
+ []
+ member _.TaskAsyncValueCancellation(newAwait: bool) =
use ewh = new ManualResetEvent(false)
let cts = new CancellationTokenSource()
let token = cts.Token
use t : Task = Task.Factory.StartNew(Func(fun () -> while not token.IsCancellationRequested do ()), token)
let cancelled = ref true
- let a =
- async {
- try
- use! _holder = Async.OnCancel(fun _ -> ewh.Set() |> ignore)
- let! v = Async.AwaitTask(t)
- return v
- // AwaitTask raises TaskCanceledException when it is canceled, it is a valid result of this test
- with
- :? TaskCanceledException ->
- ewh.Set() |> ignore // this is ok
- }
+ let a = async {
+ try
+ use! _holder = Async.OnCancel(fun _ -> ewh.Set() |> ignore)
+ let! v = t |> if newAwait then Async.Await else Async.AwaitTask
+ return v
+ // A canceled task yields TaskCanceledException via the exception continuation
+ with
+ :? TaskCanceledException ->
+ ewh.Set() |> ignore // this is ok
+ }
let t1 = Async.StartAsTask a
cts.Cancel()
ewh.WaitOne(10000) |> ignore
// Don't leave unobserved background tasks, because they can crash the test run.
t1.Wait()
- []
- member _.NonGenericTaskAsyncValue () =
+ []
+ member _.NonGenericTaskAsyncValue(newAwait: bool) =
let mutable hasBeenCalled = false
use t = Task.Factory.StartNew(Action(fun () -> hasBeenCalled <- true))
let a = async {
- do! Async.AwaitTask(t)
- return true
- }
- let result = Async.RunSynchronously(a)
- (hasBeenCalled && result) |> Assert.True
+ do! t |> if newAwait then Async.Await else Async.AwaitTask
+ return true
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True(hasBeenCalled && ok)
- []
- member _.NonGenericTaskAsyncValueException () =
+ []
+ member _.NonGenericTaskAsyncValueException(newAwait: bool) =
use t = Task.Factory.StartNew(Action(fun () -> raise <| Exception()))
let a = async {
- try
- let! v = Async.AwaitTask(t)
- return false
- with e -> return true
- }
- Async.RunSynchronously(a) |> Assert.True
+ try
+ let! v = t |> if newAwait then Async.Await else Async.AwaitTask
+ return false
+ with e -> return true
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
- []
- member _.NonGenericTaskAsyncValueCancellation () =
+ []
+ member _.NonGenericTaskAsyncValueCancellation(newAwait: bool) =
use ewh = new ManualResetEvent(false)
let cts = new CancellationTokenSource()
let token = cts.Token
use t = Task.Factory.StartNew(Action(fun () -> while not token.IsCancellationRequested do ()), token)
- let a =
- async {
- try
- use! _holder = Async.OnCancel(fun _ -> ewh.Set() |> ignore)
- let! v = Async.AwaitTask(t)
- return v
- // AwaitTask raises TaskCanceledException when it is canceled, it is a valid result of this test
- with
- :? TaskCanceledException ->
- ewh.Set() |> ignore // this is ok
- }
+ let a = async {
+ try
+ use! _holder = Async.OnCancel(fun _ -> ewh.Set() |> ignore)
+ let! v = t |> if newAwait then Async.Await else Async.AwaitTask
+ return v
+ // A canceled task yields TaskCanceledException via the exception continuation
+ with
+ :? TaskCanceledException ->
+ ewh.Set() |> ignore // this is ok
+ }
let t1 = Async.StartAsTask a
cts.Cancel()
ewh.WaitOne(10000) |> ignore
@@ -510,19 +510,192 @@ type AsyncType() =
ewh.Wait(10000) |> ignore
Assert.False hasThrown
- []
- member _.NoStackOverflowOnRecursion() =
-
+ []
+ member _.NoStackOverflowOnRecursion(newAwait: bool) =
let mutable hasThrown = false
let rec loop (x: int) = async {
- do! Task.CompletedTask |> Async.AwaitTask
+ do! Task.CompletedTask |> if newAwait then Async.Await else Async.AwaitTask
Console.WriteLine (if x = 10000 then failwith "finish" else x)
return! loop(x+1)
}
- try
- Async.RunSynchronously (loop 0)
- hasThrown <- false
+ try Async.RunSynchronously (loop 0)
+ hasThrown <- false
with Failure "finish" ->
hasThrown <- true
Assert.True hasThrown
+
+ // Both AwaitTask and Await ignore the ambient cancellation token while waiting
+ // (Same goes for the typed variants)
+ []
+ member _.``Both AwaitTask and Await ignore ambient cancellation while waiting``(newAwait) =
+ let cts = new CancellationTokenSource()
+ let tcs = TaskCompletionSource() // task that never completes
+ let res = TaskCompletionSource()
+
+ let a = async {
+ try do! tcs.Task |> if newAwait then Async.Await else Async.AwaitTask
+ res.TrySetResult true |> ignore
+ with _ -> res.TrySetResult false |> ignore
+ }
+
+ Async.Start(a, cts.Token)
+ // NOTE we only cancel during the Await/AwaitTask - the initial check would throw if we canceled before the Start()
+ cts.CancelAfter 100
+
+ // AwaitTask should NOT honor the ambient CT trigger
+ let taskCompleted = res.Task.Wait 500
+ Assert.False(taskCompleted, "Await/AwaitTask should not have responded to ambient CT cancellation")
+ tcs.TrySetResult() |> ignore // clean up
+ res.Task.Wait()
+
+ (* When an AggregateException has multiple inner exceptions, Await and AwaitTask behave identically *)
+
+ []
+ member _.``Await and AwaitTask(Task<'T>) valid AggregateException is surfaced``(newAwait) =
+ let tcs = TaskCompletionSource()
+ tcs.SetException [ ArgumentException "a" :> exn; InvalidOperationException "b" :> exn ]
+ let a = async {
+ try
+ let! _ = tcs.Task |> if newAwait then Async.Await else Async.AwaitTask
+ return false
+ with :? AggregateException as ae -> return ae.InnerExceptions.Count = 2
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ []
+ member _.``Await and AwaitTask(Task) valid AggregateException is surfaced``(newAwait) =
+ let tcs = TaskCompletionSource()
+ tcs.SetException [| ArgumentException "a" :> exn; InvalidOperationException "b" |]
+ let a = async {
+ try
+ do! tcs.Task |> if newAwait then Async.Await else Async.AwaitTask
+ return false
+ with :? AggregateException as ae -> return ae.InnerExceptions.Count = 2
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ (* Async.Await behavioral differences
+
+ The following tests demonstrate where Async.Await deliberately differs from Async.AwaitTask *)
+
+ // Async.AwaitTask(Task) surfaces the wrapping AggregateException ...
+ []
+ member _.``AwaitTask(Task) egregious AggregateException is unchanged``() =
+ let tcs = TaskCompletionSource()
+ tcs.SetException(ArgumentException "original")
+ let a = async {
+ try do! Async.AwaitTask tcs.Task
+ return false
+ with :? AggregateException -> return true
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ // ... whereas Async.Await(Task) surfaces the inner exception directly.
+ []
+ member _.``Await(Task) egregious AggregateException is unwrapped``() =
+ let tcs = TaskCompletionSource()
+ tcs.SetException(ArgumentException "original")
+ let a = async {
+ try do! Async.Await tcs.Task
+ return false
+ with :? ArgumentException as ae -> return ae.Message = "original"
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ // Async.AwaitTask(Task<'T>) surfaces the wrapping AggregateException ...
+ []
+ member _.``AwaitTask(Task<'T>) egregious AggregateException is unchanged``() =
+ let tcs = TaskCompletionSource()
+ tcs.SetException(ArgumentException "original")
+ let a = async {
+ try let! _ = Async.AwaitTask tcs.Task
+ return false
+ with :? AggregateException -> return true
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ // ... whereas Async.Await(Task<'T>) surfaces the inner exception directly.
+ []
+ member _.``Await(Task<'T>) egregious AggregateException is unwrapped``() =
+ let tcs = TaskCompletionSource()
+ tcs.SetException(ArgumentException "original")
+ let a = async {
+ try let! _ = Async.Await tcs.Task
+ return false
+ with :? ArgumentException as ae -> return ae.Message = "original"
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ (* Await(Task/Task<'T>) overloads happy path *)
+
+ []
+ member _.``Await(Task<'T>) happy path``() =
+ let a = async {
+ let! v = Async.Await(System.Threading.Tasks.Task.FromResult(42))
+ return v = 42
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ []
+ member _.``Await(Task) happy path``() =
+ let a = async {
+ do! Async.Await(System.Threading.Tasks.Task.CompletedTask)
+ return true
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+#if NETSTANDARD2_1
+ (* Await(ValueTask and ValueTask<'T>) overloads coverage of mainline behaviors *)
+
+ []
+ member _.``Await(ValueTask) happy path``() =
+ let a = async {
+ do! Async.Await(ValueTask())
+ return true
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ []
+ member _.``Await(ValueTask<'T>) happy path``() =
+ let a = async {
+ let! v = Async.Await(ValueTask(42))
+ return v = 42
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ []
+ member _.``Await(ValueTask) exception unwraps``() =
+ let tcs = TaskCompletionSource()
+ tcs.SetException(ArgumentException "original")
+ let task = ValueTask(tcs.Task :> Task)
+ let a = async {
+ try do! Async.Await task
+ return false
+ with :? ArgumentException as ae -> return ae.Message = "original"
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+
+ []
+ member _.``Await(ValueTask<'T>) exception unwraps``() =
+ let tcs = TaskCompletionSource()
+ tcs.SetException(ArgumentException "original")
+ let a = async {
+ try let! _ = Async.Await(ValueTask(tcs.Task))
+ return false
+ with :? ArgumentException as ae -> return ae.Message = "original"
+ }
+ let ok = Async.RunSynchronously a
+ Assert.True ok
+#endif
\ No newline at end of file
From 9240ff8d8252f59cc06c8d43ea97a5434301364e Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Fri, 22 May 2026 16:29:49 +0100
Subject: [PATCH 02/26] Polish xmldoc
---
src/FSharp.Core/async.fsi | 172 +++++++++++++++++++++++++++-----------
1 file changed, 123 insertions(+), 49 deletions(-)
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index 5a5a991c293..848a09ed258 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -741,42 +741,64 @@ namespace Microsoft.FSharp.Control
static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout:int -> Async
/// Creates an asynchronous computation that will wait asynchronously for the given task to complete, returning
- /// its result.
- ///
+ /// its result. Note exceptions are wrapped in ; for new
+ /// code, prefer Async.Await, which surfaces single exceptions directly.
/// The task to await.
- ///
- /// If the task yields an exception, then then the full underlying is re-raised by this function.
- ///
- /// If the task is canceled then is raised. Note
+ /// If the task is canceled then is raised. Note
/// that the task may be governed by a different cancellation token to the overall async computation
/// where the AwaitTask occurs. In practice you should normally start the task with the
/// cancellation token returned by let! ct = Async.CancellationToken, and catch
/// any at the point where the
/// overall async is started.
///
- ///
/// Awaiting Results
- ///
- ///
+ ///
+ ///
+ /// let t = Task.Run(fun () -> invalidOp "test"; 42)
+ /// async {
+ /// try
+ /// let! _ = Async.AwaitTask t
+ /// ()
+ /// with
+ /// | :? System.InvalidOperationException ->
+ /// printfn "unreachable" // will not match: exception is wrapped in AggregateException
+ /// | :? System.AggregateException as e ->
+ /// printfn $"Caught: {e.InnerException.Message}"
+ /// } |> Async.RunSynchronously
+ ///
+ /// Prints Caught: test. The InvalidOperationException branch is not reached because
+ /// exceptions from tasks are always wrapped in . Contrast with Async.Await.
+ ///
static member AwaitTask: task: Task<'T> -> Async<'T>
- /// Creates an asynchronous computation that will wait asynchronously for the given task to complete.
- ///
+ /// Creates an asynchronous computation that will wait asynchronously for the given task to complete.
+ /// Note exceptions are wrapped in ; for new
+ /// code, prefer Async.Await, which surfaces single exceptions directly.
/// The task to await.
- ///
- /// If the task yields an exception, then the full underlying is re-raised by this function.
- ///
- /// If the task is canceled then is raised. Note
+ /// If the task is canceled then is raised. Note
/// that the task may be governed by a different cancellation token to the overall async computation
/// where the AwaitTask occurs. In practice you should normally start the task with the
/// cancellation token returned by let! ct = Async.CancellationToken, and catch
/// any at the point where the
/// overall async is started.
///
- ///
/// Awaiting Results
- ///
- ///
+ ///
+ ///
+ /// let t = Task.Run(fun () -> invalidOp "test")
+ /// async {
+ /// try
+ /// do! Async.AwaitTask t
+ /// with
+ /// | :? System.InvalidOperationException ->
+ /// printfn "unreachable" // will not match: exception is wrapped in AggregateException
+ /// | :? System.AggregateException as e ->
+ /// printfn $"Caught: {e.InnerException.Message}"
+ /// } |> Async.RunSynchronously
+ ///
+ /// Prints Caught: test. The InvalidOperationException branch is not reached because
+ /// exceptions from tasks are always wrapped in . Contrast with Async.Await.
+ ///
static member AwaitTask: task: Task -> Async
/// Creates an asynchronous computation that will wait for the given task to complete and return
@@ -784,66 +806,118 @@ namespace Microsoft.FSharp.Control
///
/// The task to await.
///
- /// Exceptions from the task are surfaced directly, without wrapping in
- /// . An
- /// will only be surfaced where multiple inner exceptions are present.
+ /// Exceptions are surfaced directly: a task faulted with a single exception raises that
+ /// exception; only s carrying multiple inner exceptions are
+ /// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
+ /// , use Async.AwaitTask.
///
/// If the task is canceled then is raised.
///
///
/// Awaiting Results
///
- ///
+ ///
+ ///
+ /// let t = Task.Run(fun () -> invalidOp "test"; 42)
+ /// async {
+ /// try
+ /// let! _ = Async.Await t
+ /// ()
+ /// with
+ /// | :? System.InvalidOperationException as e ->
+ /// printfn $"Caught: {e.Message}"
+ /// | :? System.AggregateException ->
+ /// printfn "unreachable" // will not match: single exception is unwrapped
+ /// } |> Async.RunSynchronously
+ ///
+ /// Prints Caught: test. The AggregateException branch is not reached because a
+ /// single-inner exception is unwrapped. Contrast with Async.AwaitTask.
+ ///
static member Await: task: Task<'T> -> Async<'T>
/// Creates an asynchronous computation that will wait for the given task to complete.
- ///
/// The task to await.
- ///
- /// Exceptions from the task are surfaced directly, without wrapping in
- /// . An
- /// will only be surfaced where multiple inner exceptions are present.
+ /// Exceptions are surfaced directly: a task faulted with a single exception raises that
+ /// exception; only s carrying multiple inner exceptions are
+ /// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
+ /// , use Async.AwaitTask.
///
/// If the task is canceled then is raised.
///
- ///
/// Awaiting Results
- ///
- ///
+ ///
+ ///
+ /// let t = Task.Run(fun () -> invalidOp "test")
+ /// async {
+ /// try
+ /// do! Async.Await t
+ /// with
+ /// | :? System.InvalidOperationException as e ->
+ /// printfn $"Caught: {e.Message}"
+ /// | :? System.AggregateException ->
+ /// printfn "unreachable" // will not match: single exception is unwrapped
+ /// } |> Async.RunSynchronously
+ ///
+ /// Prints Caught: test. The AggregateException branch is not reached because a
+ /// single-inner exception is unwrapped. Contrast with Async.AwaitTask.
+ ///
static member Await: task: Task -> Async
#if NETSTANDARD2_1
- /// Return an asynchronous computation that will wait for the given task to complete and return
+ /// Creates an asynchronous computation that will wait for the given ValueTask to complete and return
/// its result.
- ///
/// The ValueTask to await.
- ///
- /// Exceptions from the task are surfaced directly, without wrapping in
- /// . An
- /// will only be surfaced where multiple inner exceptions are present.
+ /// Exceptions are surfaced directly: a task faulted with a single exception raises that
+ /// exception; only s carrying multiple inner exceptions are
+ /// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
+ /// , use Async.AwaitTask.
///
/// If the task is canceled then is raised.
///
- ///
/// Awaiting Results
- ///
- ///
+ ///
+ ///
+ /// let vt = ValueTask<int>(Task.Run(fun () -> invalidOp "test"; 42))
+ /// async {
+ /// try
+ /// let! _ = Async.Await vt
+ /// ()
+ /// with
+ /// | :? System.InvalidOperationException as e ->
+ /// printfn $"Caught: {e.Message}"
+ /// | :? System.AggregateException ->
+ /// printfn "unreachable" // will not match: single exception is unwrapped
+ /// } |> Async.RunSynchronously
+ ///
+ /// Prints Caught: test.
+ ///
static member Await: task: ValueTask<'T> -> Async<'T>
- /// Return an asynchronous computation that will wait for the given ValueTask to complete.
- ///
- /// The task to await.
- ///
- /// Exceptions from the task are surfaced directly, without wrapping in
- /// . An
- /// will only be surfaced where multiple inner exceptions are present.
+ /// Creates an asynchronous computation that will wait for the given ValueTask to complete.
+ /// The ValueTask to await.
+ /// Exceptions are surfaced directly: a task faulted with a single exception raises that
+ /// exception; only s carrying multiple inner exceptions are
+ /// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
+ /// , use Async.AwaitTask.
///
/// If the task is canceled then is raised.
///
- ///
/// Awaiting Results
- ///
- ///
+ ///
+ ///
+ /// let vt = ValueTask(Task.Run(fun () -> invalidOp "test"))
+ /// async {
+ /// try
+ /// do! Async.Await vt
+ /// with
+ /// | :? System.InvalidOperationException as e ->
+ /// printfn $"Caught: {e.Message}"
+ /// | :? System.AggregateException ->
+ /// printfn "unreachable" // will not match: single exception is unwrapped
+ /// } |> Async.RunSynchronously
+ ///
+ /// Prints Caught: test.
+ ///
static member Await: task: ValueTask -> Async
#endif
From 97c31eb477c4b04d5034d61a8d1cfbbb08e69f37 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Fri, 22 May 2026 16:38:40 +0100
Subject: [PATCH 03/26] Fix test expectation
---
.../LegacyLanguageService/Tests.LanguageService.QuickInfo.fs | 1 +
1 file changed, 1 insertion(+)
diff --git a/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs b/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
index bb9571afa67..3aa8abf66b6 100644
--- a/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
+++ b/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
@@ -268,6 +268,7 @@ type UsingMSBuild() =
let expectedTooltip = """
type Async =
static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
+ static member Await: task: Task<'T> -> Async<'T> + 3 overloads
static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate and 'Del: not null)
static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async
static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload
From 3ae385dead1801d7a8ceb875a098d5dc23261837 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Fri, 22 May 2026 18:17:56 +0100
Subject: [PATCH 04/26] hack to silence test fail
---
.../Tests.LanguageService.QuickInfo.fs | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs b/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
index 3aa8abf66b6..4e4e5b30940 100644
--- a/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
+++ b/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
@@ -280,7 +280,11 @@ type Async =
static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
...
Full name: Microsoft.FSharp.Control.Async""".TrimStart().Replace("\r\n", "\n")
-
+ // Hack to deal with fact that FSharp.Core's Async.Await will have 2 overloads in netstandard2.0 but 4 in netstandard2.1
+ let checkTooltip expected ((tooltip: string, span : TextSpan), (row, col)) =
+ let tooltip = tooltip.Replace("static member Await: task: Task<'T> -> Async<'T> + 1 overload",
+ "static member Await: task: Task<'T> -> Async<'T> + 3 overloads")
+ checkTooltip expected ((tooltip, span), (row, col))
this.CheckTooltip(source, "Asyn", false, checkTooltip expectedTooltip)
[]
From 76f5bad467011b0d2a6627c54b4444e50dc65401 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Fri, 22 May 2026 18:54:07 +0100
Subject: [PATCH 05/26] Apply majocha correction
---
src/FSharp.Core/async.fs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/FSharp.Core/async.fs b/src/FSharp.Core/async.fs
index ca8197490b5..6e16320f09d 100644
--- a/src/FSharp.Core/async.fs
+++ b/src/FSharp.Core/async.fs
@@ -2256,14 +2256,14 @@ type Async =
#if NETSTANDARD2_1
static member Await(task: ValueTask<'T>) : Async<'T> =
- if task.IsCompleted then
- async { return task.GetAwaiter().GetResult() }
+ if task.IsCompletedSuccessfully then
+ CreateReturnAsync(task.GetAwaiter().GetResult())
else
Async.Await(task.AsTask())
static member Await(task: ValueTask) : Async =
- if task.IsCompleted then
- async { return task.GetAwaiter().GetResult() }
+ if task.IsCompletedSuccessfully then
+ CreateReturnAsync(task.GetAwaiter().GetResult())
else
Async.Await(task.AsTask())
#endif
From cd66660642a2af43abcefe3d044c8d68d8106d32 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Fri, 22 May 2026 22:13:15 +0100
Subject: [PATCH 06/26] Remove layer of indirection
---
src/FSharp.Core/async.fs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/FSharp.Core/async.fs b/src/FSharp.Core/async.fs
index 6e16320f09d..2fce3bb6705 100644
--- a/src/FSharp.Core/async.fs
+++ b/src/FSharp.Core/async.fs
@@ -2259,13 +2259,13 @@ type Async =
if task.IsCompletedSuccessfully then
CreateReturnAsync(task.GetAwaiter().GetResult())
else
- Async.Await(task.AsTask())
+ AwaitTask true (task.AsTask())
static member Await(task: ValueTask) : Async =
if task.IsCompletedSuccessfully then
CreateReturnAsync(task.GetAwaiter().GetResult())
else
- Async.Await(task.AsTask())
+ AwaitUnitTask true (task.AsTask())
#endif
module CommonExtensions =
From ae14fcb80b01c135506502766a125ee7fc991606 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sat, 23 May 2026 21:54:07 +0100
Subject: [PATCH 07/26] add support for TaskLike
---
docs/release-notes/.FSharp.Core/11.0.100.md | 2 +-
src/FSharp.Core/async.fs | 29 ++++++++++
src/FSharp.Core/async.fsi | 46 +++++++++++++++
...p.Core.SurfaceArea.netstandard20.debug.bsl | 2 +
...Core.SurfaceArea.netstandard20.release.bsl | 2 +
...p.Core.SurfaceArea.netstandard21.debug.bsl | 2 +
...Core.SurfaceArea.netstandard21.release.bsl | 2 +
.../Microsoft.FSharp.Control/AsyncType.fs | 58 ++++++++++++++++++-
8 files changed, 141 insertions(+), 2 deletions(-)
diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md
index fe933aca0fd..219a89f06ce 100644
--- a/docs/release-notes/.FSharp.Core/11.0.100.md
+++ b/docs/release-notes/.FSharp.Core/11.0.100.md
@@ -6,4 +6,4 @@
### Added
-* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785))
+* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Also adds an SRTP overload accepting any task-like value that supports the `GetAwaiter` protocol. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785))
diff --git a/src/FSharp.Core/async.fs b/src/FSharp.Core/async.fs
index 2fce3bb6705..58f78a9891b 100644
--- a/src/FSharp.Core/async.fs
+++ b/src/FSharp.Core/async.fs
@@ -13,6 +13,7 @@ open System.Runtime.ExceptionServices
open System.Threading
open System.Threading.Tasks
open Microsoft.FSharp.Core
+open Microsoft.FSharp.Core.CompilerServices
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators
open Microsoft.FSharp.Control
open Microsoft.FSharp.Collections
@@ -2268,6 +2269,34 @@ type Async =
AwaitUnitTask true (task.AsTask())
#endif
+module AsyncTaskLikeExtensions =
+
+ type Async with
+
+ []
+ static member inline Await< ^TaskLike, ^Awaiter, 'T
+ when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
+ and ^Awaiter :> ICriticalNotifyCompletion
+ and ^Awaiter: (member get_IsCompleted: unit -> bool)
+ and ^Awaiter: (member GetResult: unit -> 'T)>
+ (task: ^TaskLike)
+ : Async<'T> =
+ Async.FromContinuations(fun (cont, econt, _ccont) ->
+ let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) task)
+
+ if (^Awaiter: (member get_IsCompleted: unit -> bool) awaiter) then
+ try
+ cont ((^Awaiter: (member GetResult: unit -> 'T) awaiter))
+ with e ->
+ econt e
+ else
+ (awaiter :> ICriticalNotifyCompletion)
+ .UnsafeOnCompleted(fun () ->
+ try
+ cont ((^Awaiter: (member GetResult: unit -> 'T) awaiter))
+ with e ->
+ econt e))
+
module CommonExtensions =
type System.IO.Stream with
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index 848a09ed258..f9cc5b21442 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -5,9 +5,11 @@ namespace Microsoft.FSharp.Control
open System
open System.Threading
open System.Threading.Tasks
+ open System.Runtime.CompilerServices
open System.Runtime.ExceptionServices
open Microsoft.FSharp.Core
+ open Microsoft.FSharp.Core.CompilerServices
open Microsoft.FSharp.Control
open Microsoft.FSharp.Collections
@@ -1213,6 +1215,50 @@ namespace Microsoft.FSharp.Control
computation:Async<'T> * ?cancellationToken:CancellationToken-> Task<'T>
+ /// A module of extension members providing support for awaiting any task-like value via the GetAwaiter pattern.
+ ///
+ /// Awaiting Results
+ []
+ module AsyncTaskLikeExtensions =
+
+ type Async with
+
+ /// Creates an asynchronous computation that will wait for the given task-like value to complete and return
+ /// its result.
+ /// The task-like value to await.
+ /// The value must satisfy the GetAwaiter pattern: it must have a GetAwaiter() method
+ /// returning an awaiter implementing
+ /// with IsCompleted and GetResult() members. Exceptions thrown by GetResult() are
+ /// propagated directly.
+ ///
+ /// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
+ /// The specific overloads for , ,
+ /// and
+ /// are preferred when the argument type is known.
+ ///
+ /// Awaiting Results
+ ///
+ ///
+ /// // A minimal custom task-like type
+ /// type MyTask<'T>(task: System.Threading.Tasks.Task<'T>) =
+ /// member _.GetAwaiter() = task.GetAwaiter()
+ ///
+ /// let myTask = MyTask(System.Threading.Tasks.Task.FromResult 42)
+ /// async {
+ /// let! result = Async.Await myTask
+ /// printfn $"Result: {result}"
+ /// } |> Async.RunSynchronously
+ ///
+ /// Prints Result: 42.
+ ///
+ []
+ static member inline Await< ^TaskLike, ^Awaiter, 'T> :
+ task: ^TaskLike -> Async<'T>
+ when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
+ and ^Awaiter :> ICriticalNotifyCompletion
+ and ^Awaiter: (member get_IsCompleted: unit -> bool)
+ and ^Awaiter: (member GetResult: unit -> 'T)
+
/// The F# compiler emits references to this type to implement F# async expressions.
///
/// Async Internals
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl
index 2b534cc247d..a1816af6912 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl
@@ -623,6 +623,8 @@ Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn I
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn TryFinally[T](Microsoft.FSharp.Control.AsyncActivation`1[T], Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit])
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn TryWith[T](Microsoft.FSharp.Control.AsyncActivation`1[T], Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[System.Exception,Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Control.FSharpAsync`1[T]]])
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.FSharpAsync`1[T] MakeAsync[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Control.AsyncActivation`1[T],Microsoft.FSharp.Control.AsyncReturn])
+Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], TTaskLike)
+Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static[TTaskLike,TAwaiter,T](TTaskLike)
Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T])
Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T])
Microsoft.FSharp.Control.CommonExtensions: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AsyncWrite(System.IO.Stream, Byte[], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl
index d2682172d43..5e1ad684f76 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl
@@ -623,6 +623,8 @@ Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn I
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn TryFinally[T](Microsoft.FSharp.Control.AsyncActivation`1[T], Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit])
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn TryWith[T](Microsoft.FSharp.Control.AsyncActivation`1[T], Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[System.Exception,Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Control.FSharpAsync`1[T]]])
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.FSharpAsync`1[T] MakeAsync[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Control.AsyncActivation`1[T],Microsoft.FSharp.Control.AsyncReturn])
+Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], TTaskLike)
+Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static[TTaskLike,TAwaiter,T](TTaskLike)
Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T])
Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T])
Microsoft.FSharp.Control.CommonExtensions: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AsyncWrite(System.IO.Stream, Byte[], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl
index ab8571b1442..b082efcca7b 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl
@@ -625,6 +625,8 @@ Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn I
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn TryFinally[T](Microsoft.FSharp.Control.AsyncActivation`1[T], Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit])
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn TryWith[T](Microsoft.FSharp.Control.AsyncActivation`1[T], Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[System.Exception,Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Control.FSharpAsync`1[T]]])
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.FSharpAsync`1[T] MakeAsync[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Control.AsyncActivation`1[T],Microsoft.FSharp.Control.AsyncReturn])
+Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], TTaskLike)
+Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static[TTaskLike,TAwaiter,T](TTaskLike)
Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T])
Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T])
Microsoft.FSharp.Control.CommonExtensions: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AsyncWrite(System.IO.Stream, Byte[], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl
index 69fc090f3fc..80d9c28153e 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl
@@ -625,6 +625,8 @@ Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn I
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn TryFinally[T](Microsoft.FSharp.Control.AsyncActivation`1[T], Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit])
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn TryWith[T](Microsoft.FSharp.Control.AsyncActivation`1[T], Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[System.Exception,Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Control.FSharpAsync`1[T]]])
Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.FSharpAsync`1[T] MakeAsync[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Control.AsyncActivation`1[T],Microsoft.FSharp.Control.AsyncReturn])
+Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], TTaskLike)
+Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static[TTaskLike,TAwaiter,T](TTaskLike)
Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T])
Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T])
Microsoft.FSharp.Control.CommonExtensions: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AsyncWrite(System.IO.Stream, Byte[], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Int32])
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
index 9aea7f42a62..04348df835d 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
@@ -698,4 +698,60 @@ type AsyncType() =
}
let ok = Async.RunSynchronously a
Assert.True ok
-#endif
\ No newline at end of file
+#endif
+
+[]
+module AsyncTaskLikeAwaitTests =
+
+ // Minimal custom task-like type wrapping Task<'T>
+ type MyTask<'T>(inner: Task<'T>) =
+ member _.GetAwaiter() = inner.GetAwaiter()
+
+ // Minimal custom unit-returning task-like
+ type MyUnitTask(inner: Task) =
+ member _.GetAwaiter() = inner.GetAwaiter()
+
+ []
+ let ``Await(task-like) happy path with result``() =
+ let result =
+ async {
+ let! v = Async.Await(MyTask(Task.FromResult 99))
+ return v
+ }
+ |> Async.RunSynchronously
+ Assert.Equal(99, result)
+
+ []
+ let ``Await(task-like) happy path unit``() =
+ async {
+ do! Async.Await(MyUnitTask(Task.CompletedTask))
+ }
+ |> Async.RunSynchronously
+
+ []
+ let ``Await(task-like) deferred completion``() =
+ let tcs = TaskCompletionSource()
+ let t =
+ async {
+ let! v = Async.Await(MyTask(tcs.Task))
+ return v
+ }
+ |> Async.StartAsTask
+ Assert.False(t.IsCompleted, "Should not be done before TCS is set")
+ tcs.SetResult 7
+ t.Wait(TimeSpan.FromSeconds 5.0) |> ignore
+ Assert.Equal(7, t.Result)
+
+ []
+ let ``Await(task-like) exception propagation``() =
+ let tcs = TaskCompletionSource()
+ let a =
+ async {
+ try let! _ = Async.Await(MyTask(tcs.Task))
+ return false
+ with :? InvalidOperationException as e ->
+ return e.Message = "boom"
+ }
+ tcs.SetException(InvalidOperationException "boom")
+ let ok = Async.RunSynchronously a
+ Assert.True ok
\ No newline at end of file
From 00b5b23bc7d749ced70d872789d2c668229e965b Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sat, 23 May 2026 23:21:21 +0100
Subject: [PATCH 08/26] Fix test expectation
---
.../LegacyLanguageService/Tests.LanguageService.QuickInfo.fs | 1 -
1 file changed, 1 deletion(-)
diff --git a/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs b/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
index 4e4e5b30940..33279cfab1f 100644
--- a/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
+++ b/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.QuickInfo.fs
@@ -277,7 +277,6 @@ type Async =
static member Catch: computation: Async<'T> -> Async>
static member Choice: computations: Async<'T option> seq -> Async<'T option>
static member FromBeginEnd: beginAction: (AsyncCallback * objnull -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads
- static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
...
Full name: Microsoft.FSharp.Control.Async""".TrimStart().Replace("\r\n", "\n")
// Hack to deal with fact that FSharp.Core's Async.Await will have 2 overloads in netstandard2.0 but 4 in netstandard2.1
From 76995f1d7eab5680a93c22b10bd9822726d266f9 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sun, 24 May 2026 02:38:19 +0100
Subject: [PATCH 09/26] Add stacktrace validation
---
.../Microsoft.FSharp.Control/AsyncType.fs | 86 ++++++++++++++++++-
1 file changed, 85 insertions(+), 1 deletion(-)
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
index 04348df835d..cc70a3c7c24 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
@@ -10,6 +10,7 @@ open FSharp.Core.UnitTests.LibraryTestFx
open Xunit
open System.Threading
open System.Threading.Tasks
+open Xunit.Internal
// Cancels default token.
[]
@@ -754,4 +755,87 @@ module AsyncTaskLikeAwaitTests =
}
tcs.SetException(InvalidOperationException "boom")
let ok = Async.RunSynchronously a
- Assert.True ok
\ No newline at end of file
+ Assert.True ok
+
+[]
+module AsyncAwaitStackTraceTests =
+
+ open System.Runtime.CompilerServices
+
+ // Minimal wrapper to route through the SRTP overload instead of the specific Task<'T> overload.
+ // Task<'T>, Task, ValueTask<'T>, and ValueTask all have higher-priority intrinsic overloads.
+ type TaskWrapper<'T>(inner: Task<'T>) =
+ member _.GetAwaiter() = inner.GetAwaiter()
+
+ // Plain function — provides a stable named frame at the outermost throw site.
+ []
+ let throwAtLevel1 () : unit = invalidOp "boom"
+
+ // Level-1 task: thin wrapper around the direct throw.
+ []
+ let level1Task () : Task = task { throwAtLevel1 () }
+
+ // Level-2 task: introduces a real async await boundary between levels 1 and 2.
+ []
+ let level2Task () : Task = task { do! level1Task () }
+
+ // Run via StartImmediateAsTask + .Wait() and return the inner exception.
+ // Using StartImmediateAsTask (not RunSynchronously) ensures that the async-layer
+ // exception machinery goes through TaskCompletionSource.SetException, which preserves
+ // the stack trace rather than rethrowing synchronously and potentially truncating it.
+ let runAndCaptureException (computation: Async) : exn =
+ // TODO swap in usage of Async.RunSynchronouslyImmediate
+ let t = Async.StartImmediateAsTask computation
+ let ae = Assert.Throws(fun () -> t.Wait())
+ ae.InnerException
+
+ // Template assertion: levels 1 and 2 must be traceable in the stack trace
+ // regardless of which Async.Await overload is used.
+ let checkTrace totalCount (e: exn) =
+ let trace = e.StackTrace
+ Assert.NotNull(trace)
+ Assert.Contains("throwAtLevel1", trace)
+ Assert.Contains("level1Task", trace)
+ Assert.Contains("level2Task", trace)
+#if !NETFRAMEWORK472 // downlevel has interstitial layers we are not seeking to characterize at this point
+ Assert.True((totalCount = trace.Split('\n').Length), trace)
+#endif
+
+ // --- Tests per overload ---
+ // The common skeleton is: build a 3-level chain (throwAtLevel1 → level1Task → level2Task),
+ // wrap the outermost level in an async block using Async.Await, run via
+ // StartImmediateAsTask + .Wait(), and assert on the resulting exception's stack trace.
+
+ []
+ let ``Await Task-of-T: all three levels visible in stack trace`` () =
+ let e = runAndCaptureException (async { do! Async.Await(level2Task()) })
+ checkTrace 3 e
+
+ []
+ let ``Await Task (non-generic): all three levels visible in stack trace`` () =
+ let e = runAndCaptureException (async { do! Async.Await(level2Task() :> Task) })
+ checkTrace 3 e
+ // Same behavior as the Task<'T> overload — see comment there.
+
+#if NETSTANDARD2_1
+ []
+ let ``Await ValueTask-of-T: all three levels visible in stack trace`` () =
+ // For a faulted ValueTask, IsCompletedSuccessfully is false; the overload falls
+ // through to AwaitTask, which takes the same path as the specific Task<'T> overload.
+ let e = runAndCaptureException (async { do! Async.Await(ValueTask(level2Task())) })
+ checkTrace 3 e
+
+ []
+ let ``Await ValueTask (non-generic): all three levels visible in stack trace`` () =
+ // Same as ValueTask<'T>: falls through to AwaitUnitTask for the non-successfully-completed case.
+ let e = runAndCaptureException (async { do! Async.Await(ValueTask(level2Task() :> Task)) })
+
+ checkTrace 3 e
+#endif
+
+ []
+ let ``Await task-like via SRTP overload: all three levels visible in stack trace`` () =
+ let e = runAndCaptureException (async { do! Async.Await(TaskWrapper(level2Task())) })
+
+ // 4 instead of 3 as current impl has an outer "at FSharp.Core.UnitTests.Control.AsyncAwaitStackTraceTests.e@836-9.Invoke(Tuple`3 tupledArg)
+ checkTrace 4 e
\ No newline at end of file
From 22022a0a95d9f9c0b1bd8f9eecd8818a1bbbf467 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 25 May 2026 14:03:17 +0100
Subject: [PATCH 10/26] Add specific TaskLike cases
---
.../Microsoft.FSharp.Control/AsyncType.fs | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
index cc70a3c7c24..8bce259b382 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
@@ -757,6 +757,29 @@ module AsyncTaskLikeAwaitTests =
let ok = Async.RunSynchronously a
Assert.True ok
+ []
+ let ``Await(YieldAwaitable) yields and resumes``() =
+ // Task.Yield() returns a YieldAwaitable which is a struct — exercises the struct-awaiter path.
+ let mutable before, after = false, false
+ async {
+ before <- true
+ do! Async.Await(Task.Yield())
+ after <- true
+ }
+ |> Async.RunSynchronously
+ Assert.True(before && after)
+
+ []
+ let ``Await(ConfiguredTaskAwaitable) from ConfigureAwait``() =
+ // task.ConfigureAwait(false) returns a ConfiguredTaskAwaitable — a common real-world task-like.
+ let result =
+ async {
+ let! v = Async.Await(Task.FromResult(42).ConfigureAwait(false))
+ return v
+ }
+ |> Async.RunSynchronously
+ Assert.Equal(42, result)
+
[]
module AsyncAwaitStackTraceTests =
From 3bf8d2f12b975fb1035be394251b0620232e4e30 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 25 May 2026 18:36:54 +0100
Subject: [PATCH 11/26] Apply even more feedback goodness
---
docs/release-notes/.FSharp.Core/11.0.100.md | 2 +-
.../FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs | 9 ++++++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md
index 219a89f06ce..efda4729c7e 100644
--- a/docs/release-notes/.FSharp.Core/11.0.100.md
+++ b/docs/release-notes/.FSharp.Core/11.0.100.md
@@ -6,4 +6,4 @@
### Added
-* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Also adds an SRTP overload accepting any task-like value that supports the `GetAwaiter` protocol. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785))
+* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Includes ValueTask support, and a SRTP-based overload accepting any task-like value that supports the `GetAwaiter` protocol. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785))
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
index 8bce259b382..defc4a3c3ff 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
@@ -10,7 +10,6 @@ open FSharp.Core.UnitTests.LibraryTestFx
open Xunit
open System.Threading
open System.Threading.Tasks
-open Xunit.Internal
// Cancels default token.
[]
@@ -816,12 +815,16 @@ module AsyncAwaitStackTraceTests =
// regardless of which Async.Await overload is used.
let checkTrace totalCount (e: exn) =
let trace = e.StackTrace
+ // stacktrace should be relatively compact and not bloat the logs, so unconditionally print it to save time analyzing regressions
+ printfn "EDI trace ===="
+ printfn "%s" trace
+ printfn "==== EDI trace"
Assert.NotNull(trace)
Assert.Contains("throwAtLevel1", trace)
Assert.Contains("level1Task", trace)
Assert.Contains("level2Task", trace)
-#if !NETFRAMEWORK472 // downlevel has interstitial layers we are not seeking to characterize at this point
- Assert.True((totalCount = trace.Split('\n').Length), trace)
+#if !NETFRAMEWORK // downlevel has interstitial layers we are not seeking to characterize at this point
+ Assert.Equal(totalCount, trace.Split('\n').Length)
#endif
// --- Tests per overload ---
From f62dc2dfa330b546049bbc7b3f776bd67d6a7737 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 13 Jul 2026 11:41:25 +0100
Subject: [PATCH 12/26] fix: Correct #if condition in tests
---
.../FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
index defc4a3c3ff..6482ccd03cf 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
@@ -653,7 +653,7 @@ type AsyncType() =
let ok = Async.RunSynchronously a
Assert.True ok
-#if NETSTANDARD2_1
+#if !NETFRAMEWORK
(* Await(ValueTask and ValueTask<'T>) overloads coverage of mainline behaviors *)
[]
@@ -843,7 +843,7 @@ module AsyncAwaitStackTraceTests =
checkTrace 3 e
// Same behavior as the Task<'T> overload — see comment there.
-#if NETSTANDARD2_1
+#if !NETFRAMEWORK
[]
let ``Await ValueTask-of-T: all three levels visible in stack trace`` () =
// For a faulted ValueTask, IsCompletedSuccessfully is false; the overload falls
From 1e853ee3e6915b0449c8440726bb397b4f0281b7 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 13 Jul 2026 12:06:37 +0100
Subject: [PATCH 13/26] fix: Replace UnsafeOnCompleted with OnCompleted
---
src/FSharp.Core/async.fs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/FSharp.Core/async.fs b/src/FSharp.Core/async.fs
index 58f78a9891b..41cc312969f 100644
--- a/src/FSharp.Core/async.fs
+++ b/src/FSharp.Core/async.fs
@@ -2290,8 +2290,8 @@ module AsyncTaskLikeExtensions =
with e ->
econt e
else
- (awaiter :> ICriticalNotifyCompletion)
- .UnsafeOnCompleted(fun () ->
+ (awaiter :> INotifyCompletion)
+ .OnCompleted(fun () ->
try
cont ((^Awaiter: (member GetResult: unit -> 'T) awaiter))
with e ->
From 5c12141fa2149e3926e76bd5864d581a2d5820fa Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 13 Jul 2026 13:10:00 +0100
Subject: [PATCH 14/26] doc: Exception semantics details per review
---
src/FSharp.Core/async.fsi | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index 84c775a3c27..05d213868d3 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -1226,15 +1226,16 @@ namespace Microsoft.FSharp.Control
/// Creates an asynchronous computation that will wait for the given task-like value to complete and return
/// its result.
/// The task-like value to await.
- /// The value must satisfy the GetAwaiter pattern: it must have a GetAwaiter() method
+ /// The value must satisfy the GetAwaiter pattern: it must have a GetAwaiter() method
/// returning an awaiter implementing
- /// with IsCompleted and GetResult() members. Exceptions thrown by GetResult() are
- /// propagated directly.
- ///
- /// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
+ /// with IsCompleted and GetResult() members.
+ /// Exceptions thrown by GetResult() are propagated directly. Unlike the
+ /// overloads, an carrying multiple inner exceptions is not preserved:
+ /// the first inner exception surfaces (standard GetResult() semantics).
+ /// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
/// The specific overloads for , ,
/// and
- /// are preferred when the argument type is known.
+ /// are preferred when the argument type is known.
///
/// Awaiting Results
///
From e1d07082b5fc17b7e1809e0310617e0bfb41f762 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 13 Jul 2026 13:53:11 +0100
Subject: [PATCH 15/26] docs: reinstate proviso about flowing CT
---
src/FSharp.Core/async.fsi | 56 ++++++++++++++++++++++++++++-----------
1 file changed, 40 insertions(+), 16 deletions(-)
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index 05d213868d3..48f20c93644 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -808,12 +808,18 @@ namespace Microsoft.FSharp.Control
///
/// The task to await.
///
- /// Exceptions are surfaced directly: a task faulted with a single exception raises that
+ ///
+ /// Exceptions are surfaced directly: a task faulted with a single exception raises that
/// exception; only s carrying multiple inner exceptions are
/// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
- /// , use Async.AwaitTask.
- ///
- /// If the task is canceled then is raised.
+ /// , use Async.AwaitTask.
+ ///
+ /// If the task is canceled then is raised.
+ ///
+ /// Note the task may be governed by a different cancellation token than the overall async computation;
+ /// typically tasks should be wired to the ambient cancellation token obtained via
+ /// let! ct = Async.CancellationToken, catching
+ /// where the overall async is started.
///
///
/// Awaiting Results
@@ -839,12 +845,18 @@ namespace Microsoft.FSharp.Control
/// Creates an asynchronous computation that will wait for the given task to complete.
/// The task to await.
- /// Exceptions are surfaced directly: a task faulted with a single exception raises that
+ ///
+ /// Exceptions are surfaced directly: a task faulted with a single exception raises that
/// exception; only s carrying multiple inner exceptions are
/// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
- /// , use Async.AwaitTask.
- ///
- /// If the task is canceled then is raised.
+ /// , use Async.AwaitTask.
+ ///
+ /// If the task is canceled then is raised.
+ ///
+ /// Note the task may be governed by a different cancellation token than the overall async computation;
+ /// typically tasks should be wired to the ambient cancellation token obtained via
+ /// let! ct = Async.CancellationToken, catching
+ /// where the overall async is started.
///
/// Awaiting Results
///
@@ -869,12 +881,18 @@ namespace Microsoft.FSharp.Control
/// Creates an asynchronous computation that will wait for the given ValueTask to complete and return
/// its result.
/// The ValueTask to await.
- /// Exceptions are surfaced directly: a task faulted with a single exception raises that
+ ///
+ /// Exceptions are surfaced directly: a task faulted with a single exception raises that
/// exception; only s carrying multiple inner exceptions are
/// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
- /// , use Async.AwaitTask.
- ///
- /// If the task is canceled then is raised.
+ /// , use Async.AwaitTask.
+ ///
+ /// If the task is canceled then is raised.
+ ///
+ /// Note the task may be governed by a different cancellation token than the overall async computation;
+ /// typically tasks should be wired to the ambient cancellation token obtained via
+ /// let! ct = Async.CancellationToken, catching
+ /// where the overall async is started.
///
/// Awaiting Results
///
@@ -897,12 +915,18 @@ namespace Microsoft.FSharp.Control
/// Creates an asynchronous computation that will wait for the given ValueTask to complete.
/// The ValueTask to await.
- /// Exceptions are surfaced directly: a task faulted with a single exception raises that
+ ///
+ /// Exceptions are surfaced directly: a task faulted with a single exception raises that
/// exception; only s carrying multiple inner exceptions are
/// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
- /// , use Async.AwaitTask.
- ///
- /// If the task is canceled then is raised.
+ /// , use Async.AwaitTask.
+ ///
+ /// If the task is canceled then is raised.
+ ///
+ /// Note the task may be governed by a different cancellation token than the overall async computation;
+ /// typically tasks should be wired to the ambient cancellation token obtained via
+ /// let! ct = Async.CancellationToken, catching
+ /// where the overall async is started.
///
/// Awaiting Results
///
From 2e532e0d131caa7416870c0f7dffc0a6c8316a77 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 13 Jul 2026 13:58:57 +0100
Subject: [PATCH 16/26] doc: Remove broken reference for netstandard2.0
---
src/FSharp.Core/async.fsi | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index 48f20c93644..75f266fbeb7 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -1257,8 +1257,12 @@ namespace Microsoft.FSharp.Control
/// overloads, an carrying multiple inner exceptions is not preserved:
/// the first inner exception surfaces (standard GetResult() semantics).
/// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
+#if NETSTANDARD2_1
/// The specific overloads for , ,
/// and
+#else
+ /// The specific overloads for and
+#endif
/// are preferred when the argument type is known.
///
/// Awaiting Results
From 197ff010c94760f508a755f048dc8a0996fa3ccf Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 13 Jul 2026 14:19:43 +0100
Subject: [PATCH 17/26] review: add comment re role of Await Tasklike
---
src/FSharp.Core/async.fsi | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index 75f266fbeb7..8d8dd494b16 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -1280,6 +1280,8 @@ namespace Microsoft.FSharp.Control
///
/// Prints Result: 42.
///
+ // NOTE Aside from being a catch-all to cover the GetAwaiter pattern,
+ // On netstandard2.0, this overload also covers ValueTask and ValueTask<'T>.
[]
static member inline Await< ^TaskLike, ^Awaiter, 'T> :
task: ^TaskLike -> Async<'T>
From 95595bdf12826dca013cba9e974da84fcf3e1790 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 13 Jul 2026 14:26:09 +0100
Subject: [PATCH 18/26] docs: add missing reference
---
src/FSharp.Core/async.fsi | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index 8d8dd494b16..1def02a541d 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -1254,6 +1254,9 @@ namespace Microsoft.FSharp.Control
/// returning an awaiter implementing
/// with IsCompleted and GetResult() members.
/// Exceptions thrown by GetResult() are propagated directly. Unlike the
+#if NETSTANDARD2_1
+ /// and
+#endif
/// overloads, an carrying multiple inner exceptions is not preserved:
/// the first inner exception surfaces (standard GetResult() semantics).
/// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
From 334a40fff81ebb1610e7e9c1e6451509f8a7c18d Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Mon, 13 Jul 2026 15:27:47 +0100
Subject: [PATCH 19/26] test: Regression for UnsafeOnCompletion
---
src/FSharp.Core/async.fs | 2 +-
.../Microsoft.FSharp.Control/AsyncType.fs | 25 +++++++++++++++++++
2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/src/FSharp.Core/async.fs b/src/FSharp.Core/async.fs
index 41cc312969f..7fb64bccbd2 100644
--- a/src/FSharp.Core/async.fs
+++ b/src/FSharp.Core/async.fs
@@ -2290,7 +2290,7 @@ module AsyncTaskLikeExtensions =
with e ->
econt e
else
- (awaiter :> INotifyCompletion)
+ (awaiter :> ICriticalNotifyCompletion)
.OnCompleted(fun () ->
try
cont ((^Awaiter: (member GetResult: unit -> 'T) awaiter))
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
index 6482ccd03cf..d47fe608ad2 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
@@ -742,6 +742,31 @@ module AsyncTaskLikeAwaitTests =
t.Wait(TimeSpan.FromSeconds 5.0) |> ignore
Assert.Equal(7, t.Result)
+ []
+ let ``Await(task-like) deferred completion preserves AsyncLocal ExecutionContext``() =
+ let asyncLocal = AsyncLocal()
+ let tcs = TaskCompletionSource()
+
+ let t =
+ Async.StartImmediateAsTask(async {
+ asyncLocal.Value <- "trace-id"
+ do! Async.Await(MyUnitTask(tcs.Task))
+ return asyncLocal.Value // should yield trace-id, *unless ExecutionContext did not propagate*
+ })
+ Assert.False(t.IsCompleted, "Should not be done before TCS is set")
+
+ // This should not pollute the continuation observed
+ asyncLocal.Value <- "root-context"
+ let completion =
+ Task.Run(fun () ->
+ asyncLocal.Value <- "completing-context" // if ExecutionContext is not propagated correctly to the continuation, it will see this
+ tcs.SetResult())
+
+ Assert.True(completion.Wait(TimeSpan.FromSeconds 5L), "Completion task did not finish in time.")
+ Assert.True(t.Wait(TimeSpan.FromSeconds 5L), "Awaited task did not finish in time.")
+ Assert.Equal("trace-id", t.Result) // Validate the chaining worked correctly
+ Assert.Equal("root-context", asyncLocal.Value) // Root level context should be preserved
+
[]
let ``Await(task-like) exception propagation``() =
let tcs = TaskCompletionSource()
From cb5c3784254687423bb3c475018d09edd2c4aaab Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sat, 25 Jul 2026 19:15:34 +0100
Subject: [PATCH 20/26] doc: Polish CL
---
docs/release-notes/.FSharp.Core/11.0.100.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md
index 835b893a9cc..326e3ebc22f 100644
--- a/docs/release-notes/.FSharp.Core/11.0.100.md
+++ b/docs/release-notes/.FSharp.Core/11.0.100.md
@@ -7,4 +7,4 @@
### Added
-* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Includes ValueTask support, and a SRTP-based overload accepting any task-like value that supports the `GetAwaiter` protocol. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785))
+* Add `Async.Await`, mirroring `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Includes `ValueTask` support, and a SRTP-based overload accepting any Task-like value that supports the `GetAwaiter` protocol. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785))
From 928ca92be1c7bf936b4d186fb5c8f30d0f81fb87 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sat, 25 Jul 2026 19:19:20 +0100
Subject: [PATCH 21/26] fix: Make test compile on ns2.0/net471
---
.../FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
index d47fe608ad2..2ce61e65968 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs
@@ -762,8 +762,8 @@ module AsyncTaskLikeAwaitTests =
asyncLocal.Value <- "completing-context" // if ExecutionContext is not propagated correctly to the continuation, it will see this
tcs.SetResult())
- Assert.True(completion.Wait(TimeSpan.FromSeconds 5L), "Completion task did not finish in time.")
- Assert.True(t.Wait(TimeSpan.FromSeconds 5L), "Awaited task did not finish in time.")
+ Assert.True(completion.Wait(TimeSpan.FromSeconds 5.), "Completion task hung?")
+ Assert.True(t.Wait(TimeSpan.FromSeconds 5.), "Awaited subject task hung?")
Assert.Equal("trace-id", t.Result) // Validate the chaining worked correctly
Assert.Equal("root-context", asyncLocal.Value) // Root level context should be preserved
From 7d6ee43a3cfd22c0cfc8e43cafee2dd9c554ba37 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sat, 25 Jul 2026 19:29:54 +0100
Subject: [PATCH 22/26] fix: Correct #if guard using wrong symbol
---
.../ByrefSafetyAnalysis/ByrefSafetyAnalysis.fs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/InferenceProcedures/ByrefSafetyAnalysis/ByrefSafetyAnalysis.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/InferenceProcedures/ByrefSafetyAnalysis/ByrefSafetyAnalysis.fs
index 40d511a028b..32f18bfb19f 100644
--- a/tests/FSharp.Compiler.ComponentTests/Conformance/InferenceProcedures/ByrefSafetyAnalysis/ByrefSafetyAnalysis.fs
+++ b/tests/FSharp.Compiler.ComponentTests/Conformance/InferenceProcedures/ByrefSafetyAnalysis/ByrefSafetyAnalysis.fs
@@ -1000,7 +1000,7 @@ type outref<'T> with
|> shouldSucceed
#endif
-#if NETSTANDARD2_1_OR_GREATER
+#if NETCOREAPP
[]
let``E_TopLevelByref_fs`` compilation =
compilation
From 5e3765757fbcd81451225c193a6c8d0466866366 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sun, 26 Jul 2026 10:13:40 +0100
Subject: [PATCH 23/26] fix: Update tooltip expected to include Await
---
.../FSharp.Compiler.Service.Tests/Tooltip/TooltipTests.Types.fs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/FSharp.Compiler.Service.Tests/Tooltip/TooltipTests.Types.fs b/tests/FSharp.Compiler.Service.Tests/Tooltip/TooltipTests.Types.fs
index e875a276073..8024644aae6 100644
--- a/tests/FSharp.Compiler.Service.Tests/Tooltip/TooltipTests.Types.fs
+++ b/tests/FSharp.Compiler.Service.Tests/Tooltip/TooltipTests.Types.fs
@@ -45,7 +45,7 @@ y.M()
[]
let ``QuickInfoForTypesWithHiddenRepresentation`` () =
let signatureListing =
- "type Async =\n static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)\n static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate and 'Del: not null)\n static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async\n static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload\n static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async\n static member CancelDefaultToken: unit -> unit\n static member Catch: computation: Async<'T> -> Async>\n static member Choice: computations: Async<'T option> seq -> Async<'T option>\n static member FromBeginEnd: beginAction: (AsyncCallback * objnull -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads\n static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>\n ..."
+ "type Async =\n static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)\n static member Await: task: Task<'T> -> Async<'T> + 3 overloads\n static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate and 'Del: not null)\n static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async\n static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload\n static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async\n static member CancelDefaultToken: unit -> unit\n static member Catch: computation: Async<'T> -> Async>\n static member Choice: computations: Async<'T option> seq -> Async<'T option>\n static member FromBeginEnd: beginAction: (AsyncCallback * objnull -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads\n ..."
assertTooltipContainsInOrder
[ signatureListing; "Full name: Microsoft.FSharp.Control.Async" ]
From a9dac9228515ced852e9a0d7916bec7f81a60764 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sun, 26 Jul 2026 10:29:20 +0100
Subject: [PATCH 24/26] fix: Adjust ifdef'd xmldoc to pass validation
---
src/FSharp.Core/async.fsi | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index 1def02a541d..e4f42820921 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -1259,14 +1259,16 @@ namespace Microsoft.FSharp.Control
#endif
/// overloads, an carrying multiple inner exceptions is not preserved:
/// the first inner exception surfaces (standard GetResult() semantics).
- /// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
#if NETSTANDARD2_1
+ ///
This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
/// The specific overloads for , ,
/// and
+ /// are preferred when the argument type is known.
#else
+ /// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
/// The specific overloads for and
-#endif
/// are preferred when the argument type is known.
+#endif
///
/// Awaiting Results
///
From 13d205f4c29a46935387b0958ab16579aacacdd0 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sun, 26 Jul 2026 10:54:02 +0100
Subject: [PATCH 25/26] fix: Update extractXmlDocBlocks to handle cond
compilation
---
src/FSharp.Core/async.fsi | 9 ++++-----
.../FSharp.Core/XmlDocumentationValidation.fs | 17 +++++++++++------
2 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi
index e4f42820921..105b54b9313 100644
--- a/src/FSharp.Core/async.fsi
+++ b/src/FSharp.Core/async.fsi
@@ -1253,22 +1253,21 @@ namespace Microsoft.FSharp.Control
/// The value must satisfy the GetAwaiter pattern: it must have a GetAwaiter() method
/// returning an awaiter implementing
/// with IsCompleted and GetResult() members.
- /// Exceptions thrown by GetResult() are propagated directly. Unlike the
+ ///
Exceptions thrown by GetResult() are propagated directly.
+ /// Unlike the
#if NETSTANDARD2_1
/// and
#endif
/// overloads, an carrying multiple inner exceptions is not preserved:
/// the first inner exception surfaces (standard GetResult() semantics).
-#if NETSTANDARD2_1
/// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
+#if NETSTANDARD2_1
/// The specific overloads for , ,
/// and
- /// are preferred when the argument type is known.
#else
- /// This overload uses statically resolved type parameters (SRTP) so it can accept any task-like type.
/// The specific overloads for and
- /// are preferred when the argument type is known.
#endif
+ /// are preferred when the argument type is known.
///
/// Awaiting Results
///
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/XmlDocumentationValidation.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/XmlDocumentationValidation.fs
index 3010ef7e95f..a977a96a306 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/XmlDocumentationValidation.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/XmlDocumentationValidation.fs
@@ -4,27 +4,32 @@ module FSharp.Core.UnitTests.XmlDocumentationValidation
open System
open System.IO
-open System.Text.RegularExpressions
open System.Xml
open Xunit
+let isConditionalDirectiveLine (trimmedLine: string) =
+ trimmedLine.StartsWith("#if")
+ || trimmedLine.StartsWith("#else")
+ || trimmedLine.StartsWith("#elif")
+ || trimmedLine.StartsWith("#endif")
+
/// Extracts XML documentation blocks from F# signature files
let extractXmlDocBlocks (content: string) =
- // Regex to match XML documentation comments (/// followed by XML content)
- let xmlDocPattern = @"^\s*///\s*(.*)$"
- let regex = Regex(xmlDocPattern, RegexOptions.Multiline)
-
let lines = content.Split([|'\n'; '\r'|], StringSplitOptions.RemoveEmptyEntries)
let mutable xmlBlocks = []
let mutable currentBlock = []
let mutable lineNumber = 0
-
+
for line in lines do
lineNumber <- lineNumber + 1
let trimmedLine = line.Trim()
if trimmedLine.StartsWith("///") then
let xmlContent = trimmedLine.Substring(3).Trim()
currentBlock <- (xmlContent, lineNumber) :: currentBlock
+ elif isConditionalDirectiveLine trimmedLine then
+ // Keep the current XML documentation block open across conditional directives.
+ // This supports docs that are split by #if/#else/#endif in .fsi files.
+ ()
else
if not (List.isEmpty currentBlock) then
xmlBlocks <- List.rev currentBlock :: xmlBlocks
From f52f2ca6cd1eb3c85d14674965312dc60a9c2fde Mon Sep 17 00:00:00 2001
From: Ruben Bartelink
Date: Sun, 26 Jul 2026 11:43:45 +0100
Subject: [PATCH 26/26] fix: Correct error message line numbering
---
.../FSharp.Core/XmlDocumentationValidation.fs | 61 +++++++++++--------
1 file changed, 37 insertions(+), 24 deletions(-)
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/XmlDocumentationValidation.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/XmlDocumentationValidation.fs
index a977a96a306..434ecf28c59 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/XmlDocumentationValidation.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/XmlDocumentationValidation.fs
@@ -15,31 +15,44 @@ let isConditionalDirectiveLine (trimmedLine: string) =
/// Extracts XML documentation blocks from F# signature files
let extractXmlDocBlocks (content: string) =
- let lines = content.Split([|'\n'; '\r'|], StringSplitOptions.RemoveEmptyEntries)
- let mutable xmlBlocks = []
- let mutable currentBlock = []
- let mutable lineNumber = 0
+ seq {
+ let currentBlock = ResizeArray<_>()
+ let tryFlushCurrentBlock () =
+ if currentBlock.Count > 0 then
+ let block = currentBlock |> Seq.toList
+ currentBlock.Clear()
+ Some block
+ else
+ None
- for line in lines do
- lineNumber <- lineNumber + 1
- let trimmedLine = line.Trim()
- if trimmedLine.StartsWith("///") then
- let xmlContent = trimmedLine.Substring(3).Trim()
- currentBlock <- (xmlContent, lineNumber) :: currentBlock
- elif isConditionalDirectiveLine trimmedLine then
- // Keep the current XML documentation block open across conditional directives.
- // This supports docs that are split by #if/#else/#endif in .fsi files.
- ()
- else
- if not (List.isEmpty currentBlock) then
- xmlBlocks <- List.rev currentBlock :: xmlBlocks
- currentBlock <- []
-
- // Don't forget the last block if file ends with XML comments
- if not (List.isEmpty currentBlock) then
- xmlBlocks <- List.rev currentBlock :: xmlBlocks
-
- List.rev xmlBlocks
+ use reader = new StringReader(content)
+ let mutable lineNumber = 0
+ let mutable line = reader.ReadLine()
+
+ while not (isNull line) do
+ lineNumber <- lineNumber + 1
+ let trimmed = line.Trim()
+
+ if trimmed.StartsWith("///") then
+ let xmlContent = trimmed.Substring(3).Trim()
+ if not (String.IsNullOrWhiteSpace xmlContent) then
+ currentBlock.Add((xmlContent, lineNumber))
+ elif isConditionalDirectiveLine trimmed || trimmed.Length = 0 then
+ // Keep the current XML documentation block open across conditional directives and blank lines
+ // Handles docs that have internal #if/#else/#endif guards within xmldoc blocks to cover TFM variations.
+ ()
+ else
+ match tryFlushCurrentBlock () with
+ | Some block -> yield block
+ | None -> ()
+
+ line <- reader.ReadLine()
+
+ // Don't forget the last block if file ends with XML comments
+ match tryFlushCurrentBlock () with
+ | Some block -> yield block
+ | None -> ()
+ }
/// Validates that XML content is well-formed
let validateXmlBlock (xmlLines: (string * int) list) =