Skip to content

Commit 93649f6

Browse files
committed
feat(Async): Add exception-unwrapping Await
1 parent bdb847a commit 93649f6

5 files changed

Lines changed: 410 additions & 118 deletions

File tree

docs/release-notes/.FSharp.Core/11.0.100.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22

33
* 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))
44
* Move `Async.StartChild` to the "Starting Async Computations" docs category alongside `Async.StartChildAsTask`. ([Issue #19667](https://github.com/dotnet/fsharp/issues/19667))
5+
6+
### Added
7+
8+
* 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))

src/FSharp.Core/async.fs

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,16 +1210,30 @@ module AsyncPrimitives =
12101210

12111211
task
12121212

1213+
// Used by Async.Await path to elide egregious AggregateException wrapping
1214+
[<DebuggerHidden>]
1215+
let UnwrapExn (exn: AggregateException) =
1216+
if exn.InnerExceptions.Count = 1 then
1217+
exn.InnerExceptions[0]
1218+
else
1219+
exn
1220+
12131221
// Call the appropriate continuation on completion of a task
12141222
[<DebuggerHidden>]
1215-
let OnTaskCompleted (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) =
1223+
let OnTaskCompleted unwrap (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) =
12161224
assert completedTask.IsCompleted
12171225

12181226
if completedTask.IsCanceled then
12191227
let edi = ExceptionDispatchInfo.Capture(TaskCanceledException completedTask)
12201228
ctxt.econt edi
12211229
elif completedTask.IsFaulted then
1222-
let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception
1230+
let e =
1231+
if unwrap then
1232+
UnwrapExn completedTask.Exception
1233+
else
1234+
completedTask.Exception
1235+
1236+
let edi = ExceptionDispatchInfo.RestoreOrCapture e
12231237
ctxt.econt edi
12241238
else
12251239
ctxt.cont completedTask.Result
@@ -1229,14 +1243,20 @@ module AsyncPrimitives =
12291243
// the overall async (they may be governed by different cancellation tokens, or
12301244
// the task may not have a cancellation token at all).
12311245
[<DebuggerHidden>]
1232-
let OnUnitTaskCompleted (completedTask: Task) (ctxt: AsyncActivation<unit>) =
1246+
let OnUnitTaskCompleted unwrap (completedTask: Task) (ctxt: AsyncActivation<unit>) =
12331247
assert completedTask.IsCompleted
12341248

12351249
if completedTask.IsCanceled then
12361250
let edi = ExceptionDispatchInfo.Capture(TaskCanceledException(completedTask))
12371251
ctxt.econt edi
12381252
elif completedTask.IsFaulted then
1239-
let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception
1253+
let e =
1254+
if unwrap then
1255+
UnwrapExn completedTask.Exception
1256+
else
1257+
completedTask.Exception
1258+
1259+
let edi = ExceptionDispatchInfo.RestoreOrCapture e
12401260
ctxt.econt edi
12411261
else
12421262
ctxt.cont ()
@@ -1246,10 +1266,10 @@ module AsyncPrimitives =
12461266
// completing the task. This will install a new trampoline on that thread and continue the
12471267
// execution of the async there.
12481268
[<DebuggerHidden>]
1249-
let AttachContinuationToTask (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
1269+
let AttachContinuationToTask unwrap (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
12501270
task.ContinueWith(
12511271
Action<Task<'T>>(fun completedTask ->
1252-
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted completedTask ctxt)
1272+
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted unwrap completedTask ctxt)
12531273
|> unfake),
12541274
TaskContinuationOptions.ExecuteSynchronously
12551275
)
@@ -1261,16 +1281,36 @@ module AsyncPrimitives =
12611281
// completing the task. This will install a new trampoline on that thread and continue the
12621282
// execution of the async there.
12631283
[<DebuggerHidden>]
1264-
let AttachContinuationToUnitTask (task: Task) (ctxt: AsyncActivation<unit>) =
1284+
let AttachContinuationToUnitTask unwrap (task: Task) (ctxt: AsyncActivation<unit>) =
12651285
task.ContinueWith(
12661286
Action<Task>(fun completedTask ->
1267-
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnUnitTaskCompleted completedTask ctxt)
1287+
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnUnitTaskCompleted unwrap completedTask ctxt)
12681288
|> unfake),
12691289
TaskContinuationOptions.ExecuteSynchronously
12701290
)
12711291
|> ignore
12721292
|> fake
12731293

1294+
let AwaitTask unwrap (task: Task<'T>) =
1295+
MakeAsyncWithCancelCheck(fun ctxt ->
1296+
if task.IsCompleted then
1297+
// Run synchronously without installing new trampoline
1298+
OnTaskCompleted unwrap task ctxt
1299+
else
1300+
// Continue asynchronously, via syncContext if necessary, installing new trampoline
1301+
let ctxt = DelimitSyncContext ctxt
1302+
ctxt.ProtectCode(fun () -> AttachContinuationToTask unwrap task ctxt))
1303+
1304+
let AwaitUnitTask unwrap (task: Task) =
1305+
MakeAsyncWithCancelCheck(fun ctxt ->
1306+
if task.IsCompleted then
1307+
// Continue synchronously without installing new trampoline
1308+
OnUnitTaskCompleted unwrap task ctxt
1309+
else
1310+
// Continue asynchronously, via syncContext if necessary, installing new trampoline
1311+
let ctxt = DelimitSyncContext ctxt
1312+
ctxt.ProtectCode(fun () -> AttachContinuationToUnitTask unwrap task ctxt))
1313+
12741314
/// Removes a registration places on a cancellation token
12751315
let DisposeCancellationRegistration (registration: byref<CancellationTokenRegistration option>) =
12761316
match registration with
@@ -2203,24 +2243,30 @@ type Async =
22032243
CreateWhenCancelledAsync compensation computation
22042244

22052245
static member AwaitTask(task: Task<'T>) : Async<'T> =
2206-
MakeAsyncWithCancelCheck(fun ctxt ->
2207-
if task.IsCompleted then
2208-
// Run synchronously without installing new trampoline
2209-
OnTaskCompleted task ctxt
2210-
else
2211-
// Continue asynchronously, via syncContext if necessary, installing new trampoline
2212-
let ctxt = DelimitSyncContext ctxt
2213-
ctxt.ProtectCode(fun () -> AttachContinuationToTask task ctxt))
2246+
AwaitTask false task
22142247

22152248
static member AwaitTask(task: Task) : Async<unit> =
2216-
MakeAsyncWithCancelCheck(fun ctxt ->
2217-
if task.IsCompleted then
2218-
// Continue synchronously without installing new trampoline
2219-
OnUnitTaskCompleted task ctxt
2220-
else
2221-
// Continue asynchronously, via syncContext if necessary, installing new trampoline
2222-
let ctxt = DelimitSyncContext ctxt
2223-
ctxt.ProtectCode(fun () -> AttachContinuationToUnitTask task ctxt))
2249+
AwaitUnitTask false task
2250+
2251+
static member Await(task: Task<'T>) : Async<'T> =
2252+
AwaitTask true task
2253+
2254+
static member Await(task: Task) : Async<unit> =
2255+
AwaitUnitTask true task
2256+
2257+
#if NETSTANDARD2_1
2258+
static member Await(task: ValueTask<'T>) : Async<'T> =
2259+
if task.IsCompleted then
2260+
async { return task.GetAwaiter().GetResult() }
2261+
else
2262+
Async.Await(task.AsTask())
2263+
2264+
static member Await(task: ValueTask) : Async<unit> =
2265+
if task.IsCompleted then
2266+
async { return task.GetAwaiter().GetResult() }
2267+
else
2268+
Async.Await(task.AsTask())
2269+
#endif
22242270

22252271
module CommonExtensions =
22262272

src/FSharp.Core/async.fsi

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -740,15 +740,14 @@ namespace Microsoft.FSharp.Control
740740
/// <example-tbd></example-tbd>
741741
static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
742742

743-
/// <summary>Return an asynchronous computation that will wait for the given task to complete and return
743+
/// <summary>Creates an asynchronous computation that will wait asynchronously for the given task to complete, returning
744744
/// its result.</summary>
745745
///
746746
/// <param name="task">The task to await.</param>
747747
///
748-
/// <remarks>If an exception occurs in the asynchronous computation then an exception is re-raised by this
749-
/// function.
748+
/// <remarks>If the task yields an exception, then then the full underlying <see cref="T:System.AggregateException"/> is re-raised by this function.
750749
///
751-
/// If the task is cancelled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised. Note
750+
/// If the task is canceled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised. Note
752751
/// that the task may be governed by a different cancellation token to the overall async computation
753752
/// where the AwaitTask occurs. In practice you should normally start the task with the
754753
/// cancellation token returned by <c>let! ct = Async.CancellationToken</c>, and catch
@@ -761,15 +760,13 @@ namespace Microsoft.FSharp.Control
761760
/// <example-tbd></example-tbd>
762761
static member AwaitTask: task: Task<'T> -> Async<'T>
763762

764-
/// <summary>Return an asynchronous computation that will wait for the given task to complete and return
765-
/// its result.</summary>
763+
/// <summary>Creates an asynchronous computation that will wait asynchronously for the given task to complete.</summary>
766764
///
767765
/// <param name="task">The task to await.</param>
768766
///
769-
/// <remarks>If an exception occurs in the asynchronous computation then an exception is re-raised by this
770-
/// function.
767+
/// <remarks>If the task yields an exception, then the full underlying <see cref="T:System.AggregateException"/> is re-raised by this function.
771768
///
772-
/// If the task is cancelled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised. Note
769+
/// If the task is canceled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised. Note
773770
/// that the task may be governed by a different cancellation token to the overall async computation
774771
/// where the AwaitTask occurs. In practice you should normally start the task with the
775772
/// cancellation token returned by <c>let! ct = Async.CancellationToken</c>, and catch
@@ -782,6 +779,74 @@ namespace Microsoft.FSharp.Control
782779
/// <example-tbd></example-tbd>
783780
static member AwaitTask: task: Task -> Async<unit>
784781

782+
/// <summary>Creates an asynchronous computation that will wait for the given task to complete and return
783+
/// its result.</summary>
784+
///
785+
/// <param name="task">The task to await.</param>
786+
///
787+
/// <remarks>Exceptions from the task are surfaced directly, without wrapping in
788+
/// <see cref="T:System.AggregateException"/>. An <see cref="T:System.AggregateException"/>
789+
/// will only be surfaced where multiple inner exceptions are present.
790+
///
791+
/// If the task is canceled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised.
792+
/// </remarks>
793+
///
794+
/// <category index="2">Awaiting Results</category>
795+
///
796+
/// <example-tbd></example-tbd>
797+
static member Await: task: Task<'T> -> Async<'T>
798+
799+
/// <summary>Creates an asynchronous computation that will wait for the given task to complete.</summary>
800+
///
801+
/// <param name="task">The task to await.</param>
802+
///
803+
/// <remarks>Exceptions from the task are surfaced directly, without wrapping in
804+
/// <see cref="T:System.AggregateException"/>. An <see cref="T:System.AggregateException"/>
805+
/// will only be surfaced where multiple inner exceptions are present.
806+
///
807+
/// If the task is canceled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised.
808+
/// </remarks>
809+
///
810+
/// <category index="2">Awaiting Results</category>
811+
///
812+
/// <example-tbd></example-tbd>
813+
static member Await: task: Task -> Async<unit>
814+
815+
#if NETSTANDARD2_1
816+
/// <summary>Return an asynchronous computation that will wait for the given task to complete and return
817+
/// its result.</summary>
818+
///
819+
/// <param name="task">The <c>ValueTask</c> to await.</param>
820+
///
821+
/// <remarks>Exceptions from the task are surfaced directly, without wrapping in
822+
/// <see cref="T:System.AggregateException"/>. An <see cref="T:System.AggregateException"/>
823+
/// will only be surfaced where multiple inner exceptions are present.
824+
///
825+
/// If the task is canceled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised.
826+
/// </remarks>
827+
///
828+
/// <category index="2">Awaiting Results</category>
829+
///
830+
/// <example-tbd></example-tbd>
831+
static member Await: task: ValueTask<'T> -> Async<'T>
832+
833+
/// <summary>Return an asynchronous computation that will wait for the given <c>ValueTask</c> to complete.</summary>
834+
///
835+
/// <param name="task">The task to await.</param>
836+
///
837+
/// <remarks>Exceptions from the task are surfaced directly, without wrapping in
838+
/// <see cref="T:System.AggregateException"/>. An <see cref="T:System.AggregateException"/>
839+
/// will only be surfaced where multiple inner exceptions are present.
840+
///
841+
/// If the task is canceled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised.
842+
/// </remarks>
843+
///
844+
/// <category index="2">Awaiting Results</category>
845+
///
846+
/// <example-tbd></example-tbd>
847+
static member Await: task: ValueTask -> Async<unit>
848+
#endif
849+
785850
/// <summary>
786851
/// Creates an asynchronous computation that will sleep for the given time. This is scheduled
787852
/// using a System.Threading.Timer object. The operation will not block operating system threads

tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,8 @@ Microsoft.FSharp.Control.EventModule: Void Add[T,TDel](Microsoft.FSharp.Core.FSh
644644
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])
645645
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])
646646
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]]])
647+
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Await(System.Threading.Tasks.Task)
648+
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Await(System.Threading.Tasks.ValueTask)
647649
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AwaitTask(System.Threading.Tasks.Task)
648650
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T])
649651
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[]
661663
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])
662664
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T[]] Sequential[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Control.FSharpAsync`1[T]])
663665
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]])
666+
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.Task`1[T])
667+
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.ValueTask`1[T])
664668
Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitTask[T](System.Threading.Tasks.Task`1[T])
665669
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]])
666670
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]])

0 commit comments

Comments
 (0)