Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
94 changes: 70 additions & 24 deletions src/FSharp.Core/async.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1210,16 +1210,30 @@ module AsyncPrimitives =

task

// Used by Async.Await path to elide egregious AggregateException wrapping
[<DebuggerHidden>]
let UnwrapExn (exn: AggregateException) =
if exn.InnerExceptions.Count = 1 then
exn.InnerExceptions[0]
else
exn

// Call the appropriate continuation on completion of a task
[<DebuggerHidden>]
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
Expand All @@ -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).
[<DebuggerHidden>]
let OnUnitTaskCompleted (completedTask: Task) (ctxt: AsyncActivation<unit>) =
let OnUnitTaskCompleted unwrap (completedTask: Task) (ctxt: AsyncActivation<unit>) =
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 ()
Expand All @@ -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.
[<DebuggerHidden>]
let AttachContinuationToTask (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
let AttachContinuationToTask unwrap (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
task.ContinueWith(
Action<Task<'T>>(fun completedTask ->
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted completedTask ctxt)
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted unwrap completedTask ctxt)
|> unfake),
TaskContinuationOptions.ExecuteSynchronously
)
Expand All @@ -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.
[<DebuggerHidden>]
let AttachContinuationToUnitTask (task: Task) (ctxt: AsyncActivation<unit>) =
let AttachContinuationToUnitTask unwrap (task: Task) (ctxt: AsyncActivation<unit>) =
task.ContinueWith(
Action<Task>(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<CancellationTokenRegistration option>) =
match registration with
Expand Down Expand Up @@ -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<unit> =
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<unit> =
AwaitUnitTask true task

#if NETSTANDARD2_1
static member Await(task: ValueTask<'T>) : Async<'T> =
if task.IsCompletedSuccessfully then
CreateReturnAsync(task.GetAwaiter().GetResult())
else
AwaitTask true (task.AsTask())

static member Await(task: ValueTask) : Async<unit> =
if task.IsCompletedSuccessfully then
CreateReturnAsync(task.GetAwaiter().GetResult())
else
AwaitUnitTask true (task.AsTask())
#endif

module CommonExtensions =

Expand Down
185 changes: 162 additions & 23 deletions src/FSharp.Core/async.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -740,47 +740,186 @@ namespace Microsoft.FSharp.Control
/// <example-tbd></example-tbd>
static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout:int -> Async<bool>

/// <summary>Return an asynchronous computation that will wait for the given task to complete and return
/// its result.</summary>
///
/// <summary>Creates an asynchronous computation that will wait asynchronously for the given task to complete, returning
/// its result. Note exceptions are wrapped in <see cref="T:System.AggregateException"/>; for new
/// code, prefer <c>Async.Await</c>, which surfaces single exceptions directly.</summary>
/// <param name="task">The task to await.</param>
///
/// <remarks>If an exception occurs in the asynchronous computation then an exception is re-raised by this
/// function.
///
/// If the task is cancelled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised. Note
/// <remarks>If the task is canceled then <see cref="T:System.Threading.Tasks.TaskCanceledException"/> is raised. Note
/// that the task may be governed by a different cancellation token to the overall async computation
Comment thread
bartelink marked this conversation as resolved.
/// where the AwaitTask occurs. In practice you should normally start the task with the
/// cancellation token returned by <c>let! ct = Async.CancellationToken</c>, and catch
/// any <see cref="F:System.Threading.Tasks.TaskCanceledException"/> at the point where the
/// any <see cref="T:System.Threading.Tasks.TaskCanceledException"/> at the point where the
/// overall async is started.
/// </remarks>
///
/// <category index="2">Awaiting Results</category>
///
/// <example-tbd></example-tbd>
/// <example id="awaittask-1">
/// <code lang="fsharp">
/// 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
/// </code>
/// Prints <c>Caught: test</c>. The <c>InvalidOperationException</c> branch is not reached because
/// exceptions from tasks are always wrapped in <see cref="T:System.AggregateException"/>. Contrast with <c>Async.Await</c>.
/// </example>
static member AwaitTask: task: Task<'T> -> Async<'T>

/// <summary>Return an asynchronous computation that will wait for the given task to complete and return
/// its result.</summary>
///
/// <summary>Creates an asynchronous computation that will wait asynchronously for the given task to complete.
/// Note exceptions are wrapped in <see cref="T:System.AggregateException"/>; for new
/// code, prefer <c>Async.Await</c>, which surfaces single exceptions directly.</summary>
/// <param name="task">The task to await.</param>
///
/// <remarks>If an exception occurs in the asynchronous computation then an exception is re-raised by this
/// function.
///
/// If the task is cancelled then <see cref="F:System.Threading.Tasks.TaskCanceledException"/> is raised. Note
/// <remarks>If the task is canceled then <see cref="T:System.Threading.Tasks.TaskCanceledException"/> 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 <c>let! ct = Async.CancellationToken</c>, and catch
/// any <see cref="F:System.Threading.Tasks.TaskCanceledException"/> at the point where the
/// any <see cref="T:System.Threading.Tasks.TaskCanceledException"/> at the point where the
/// overall async is started.
/// </remarks>
/// <category index="2">Awaiting Results</category>
/// <example id="awaittask-2">
/// <code lang="fsharp">
/// 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
/// </code>
/// Prints <c>Caught: test</c>. The <c>InvalidOperationException</c> branch is not reached because
/// exceptions from tasks are always wrapped in <see cref="T:System.AggregateException"/>. Contrast with <c>Async.Await</c>.
/// </example>
static member AwaitTask: task: Task -> Async<unit>

/// <summary>Creates an asynchronous computation that will wait for the given task to complete and return
/// its result.</summary>
///
/// <param name="task">The task to await.</param>
///
/// <remarks>Exceptions are surfaced directly: a task faulted with a single exception raises that
/// exception; only <see cref="T:System.AggregateException"/>s carrying multiple inner exceptions are
/// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
/// <see cref="T:System.AggregateException"/>, use <c>Async.AwaitTask</c>.
///
/// If the task is canceled then <see cref="T:System.Threading.Tasks.TaskCanceledException"/> is raised.
/// </remarks>
///
/// <category index="2">Awaiting Results</category>
///
/// <example-tbd></example-tbd>
static member AwaitTask: task: Task -> Async<unit>
/// <example id="await-task-1">
/// <code lang="fsharp">
/// 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
/// </code>
/// Prints <c>Caught: test</c>. The <c>AggregateException</c> branch is not reached because a
/// single-inner exception is unwrapped. Contrast with <c>Async.AwaitTask</c>.
/// </example>
static member Await: task: Task<'T> -> Async<'T>

/// <summary>Creates an asynchronous computation that will wait for the given task to complete.</summary>
/// <param name="task">The task to await.</param>
/// <remarks>Exceptions are surfaced directly: a task faulted with a single exception raises that
/// exception; only <see cref="T:System.AggregateException"/>s carrying multiple inner exceptions are
/// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
/// <see cref="T:System.AggregateException"/>, use <c>Async.AwaitTask</c>.
///
/// If the task is canceled then <see cref="T:System.Threading.Tasks.TaskCanceledException"/> is raised.
/// </remarks>
/// <category index="2">Awaiting Results</category>
/// <example id="await-task-2">
/// <code lang="fsharp">
/// 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
/// </code>
/// Prints <c>Caught: test</c>. The <c>AggregateException</c> branch is not reached because a
/// single-inner exception is unwrapped. Contrast with <c>Async.AwaitTask</c>.
/// </example>
static member Await: task: Task -> Async<unit>

#if NETSTANDARD2_1
/// <summary>Creates an asynchronous computation that will wait for the given <c>ValueTask</c> to complete and return
/// its result.</summary>
/// <param name="task">The <c>ValueTask</c> to await.</param>
/// <remarks>Exceptions are surfaced directly: a task faulted with a single exception raises that
/// exception; only <see cref="T:System.AggregateException"/>s carrying multiple inner exceptions are
/// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
/// <see cref="T:System.AggregateException"/>, use <c>Async.AwaitTask</c>.
///
/// If the task is canceled then <see cref="T:System.Threading.Tasks.TaskCanceledException"/> is raised.
/// </remarks>
/// <category index="2">Awaiting Results</category>
/// <example id="await-valuetask-1">
/// <code lang="fsharp">
/// let vt = ValueTask&lt;int&gt;(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
/// </code>
/// Prints <c>Caught: test</c>.
/// </example>
static member Await: task: ValueTask<'T> -> Async<'T>

/// <summary>Creates an asynchronous computation that will wait for the given <c>ValueTask</c> to complete.</summary>
/// <param name="task">The <c>ValueTask</c> to await.</param>
/// <remarks>Exceptions are surfaced directly: a task faulted with a single exception raises that
/// exception; only <see cref="T:System.AggregateException"/>s carrying multiple inner exceptions are
/// re-raised as-is. For the legacy behavior of uniformly presenting the raw underlying
/// <see cref="T:System.AggregateException"/>, use <c>Async.AwaitTask</c>.
///
/// If the task is canceled then <see cref="T:System.Threading.Tasks.TaskCanceledException"/> is raised.
/// </remarks>
/// <category index="2">Awaiting Results</category>
/// <example id="await-valuetask-2">
/// <code lang="fsharp">
/// 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
/// </code>
/// Prints <c>Caught: test</c>.
/// </example>
static member Await: task: ValueTask -> Async<unit>
#endif

/// <summary>
/// Creates an asynchronous computation that will sleep for the given time. This is scheduled
Expand Down
Loading
Loading