From c0c1f66c6414ff92bd9c1f8255a05ca8c9422719 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 21 May 2026 14:20:27 +0100 Subject: [PATCH 01/11] 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 70bbaae06fe..047d557be84 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -2,3 +2,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)) + +### 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 f09ed498bcdf86c1096fa7f97df5e16e9b174960 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 22 May 2026 16:29:49 +0100 Subject: [PATCH 02/11] 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 b0ce16fcdbae5a938385699d39907fbafaebb454 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 22 May 2026 16:38:40 +0100 Subject: [PATCH 03/11] 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 644728d48b767dd8a15ee5b140aef586e46f7e5a Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 22 May 2026 18:17:56 +0100 Subject: [PATCH 04/11] 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 41eb563784f158787bc5b71e874e271722640e19 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 22 May 2026 18:54:07 +0100 Subject: [PATCH 05/11] 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 885c40fdc25b169ad70dbb7c4de794a0196613d3 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 22 May 2026 22:13:15 +0100 Subject: [PATCH 06/11] 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 4b649c4a7971d746b12f314a52223b29c12d0a07 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sat, 23 May 2026 21:54:07 +0100 Subject: [PATCH 07/11] 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 047d557be84..9974d287b16 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -5,4 +5,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 c09cd0eb840b7643364e1e73f7be43c6faa02a86 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sat, 23 May 2026 23:21:21 +0100 Subject: [PATCH 08/11] 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 50814f54840ad1a4111ff2d74680e2e6e58c1f5e Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 24 May 2026 02:38:19 +0100 Subject: [PATCH 09/11] 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 d31a0e4f15d9a65c8cddcef3a84bc5da3c2bb6b2 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 25 May 2026 14:03:17 +0100 Subject: [PATCH 10/11] 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 4b7deb39f098cc5ea3eca10cc2ef10fe66c06f24 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 25 May 2026 18:36:54 +0100 Subject: [PATCH 11/11] 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 9974d287b16..6e60de2a2e3 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -5,4 +5,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 ---