From 747e3f16fe50c925185d39beae193db71935b37f Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:59:33 +0200 Subject: [PATCH] Align Async.Await cancellation with fslang-suggestions #840 A cancelled Task is now surfaced through the exception continuation as a TaskCanceledException (ec) instead of the cancellation continuation (cc), so it can be caught by an ordinary try/with around the Await, matching C# await and Async.AwaitTask. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/FSharpPlus/Extensions/Extensions.fs | 8 ++++---- tests/FSharpPlus.Tests/Asyncs.fs | 10 ++++++---- tests/FSharpPlus.Tests/Task.fs | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/FSharpPlus/Extensions/Extensions.fs b/src/FSharpPlus/Extensions/Extensions.fs index d89ed9a85..cd5748e12 100644 --- a/src/FSharpPlus/Extensions/Extensions.fs +++ b/src/FSharpPlus/Extensions/Extensions.fs @@ -163,13 +163,13 @@ module Extensions = /// at the point where the overall async is started. /// static member Await (task: Task<'T>) : Async<'T> = - Async.FromContinuations (fun (sc, ec, cc) -> + Async.FromContinuations (fun (sc, ec, _) -> task.ContinueWith (fun (task: Task<'T>) -> if task.IsFaulted then let e = Unchecked.nonNull task.Exception if e.InnerExceptions.Count = 1 then ec e.InnerExceptions[0] else ec e - elif task.IsCanceled then cc (TaskCanceledException ()) + elif task.IsCanceled then ec (TaskCanceledException ()) else sc task.Result) |> ignore) @@ -190,13 +190,13 @@ module Extensions = /// at the point where the overall async is started. /// static member Await (task: Task) : Async = - Async.FromContinuations (fun (sc, ec, cc) -> + Async.FromContinuations (fun (sc, ec, _) -> task.ContinueWith (fun (task: Task) -> if task.IsFaulted then let e = Unchecked.nonNull task.Exception if e.InnerExceptions.Count = 1 then ec e.InnerExceptions[0] else ec e - elif task.IsCanceled then cc (TaskCanceledException ()) + elif task.IsCanceled then ec (TaskCanceledException ()) else sc ()) |> ignore) diff --git a/tests/FSharpPlus.Tests/Asyncs.fs b/tests/FSharpPlus.Tests/Asyncs.fs index 653c01056..8369ba07f 100644 --- a/tests/FSharpPlus.Tests/Asyncs.fs +++ b/tests/FSharpPlus.Tests/Asyncs.fs @@ -41,8 +41,9 @@ module Async = let t2 = createAsync true 0 2 let t3 = createAsync true 0 3 - let c = new CancellationToken true - let t4 = Task.FromCanceled c |> Async.Await + // A genuinely cancelled async: Async.Await no longer surfaces a cancelled Task as a + // cancellation, it raises TaskCanceledException (fslang-suggestions #840). + let t4 : Async = Async.FromContinuations (fun (_, _, cc) -> cc (OperationCanceledException ())) let t5 = createAsync false 0 5 let t6 = createAsync false 0 6 @@ -71,8 +72,9 @@ module Async = let t2 = createAsync true 10 2 let t3 = createAsync true 30 3 - let c = new CancellationToken true - let t4 = Task.FromCanceled c |> Async.Await + // A genuinely cancelled async: Async.Await no longer surfaces a cancelled Task as a + // cancellation, it raises TaskCanceledException (fslang-suggestions #840). + let t4 : Async = Async.FromContinuations (fun (_, _, cc) -> cc (OperationCanceledException ())) let t5 = createAsync false 20 5 let t6 = createAsync false 10 6 diff --git a/tests/FSharpPlus.Tests/Task.fs b/tests/FSharpPlus.Tests/Task.fs index 5eb10cca3..4591569a9 100644 --- a/tests/FSharpPlus.Tests/Task.fs +++ b/tests/FSharpPlus.Tests/Task.fs @@ -273,6 +273,30 @@ module Task = Assert.AreEqual (e0, e1, "Original exception is not the same as that extracted from the Async") Assert.AreEqual (e1, e2, "The exception extracted from the Async is not the same as that extracted from the roundtripped Task") + [] + let awaitOfCancelledTaskRaisesTaskCanceledException () = + // A cancelled Task is surfaced through the exception continuation as a TaskCanceledException, so it can be + // caught with an ordinary try/with around the Await, matching C# await (fslang-suggestions #840). + let ct = CancellationToken true + + let caughtGeneric = + async { + try + let! _ = Async.Await (Task.FromCanceled ct) + return false + with :? TaskCanceledException -> return true } + |> Async.RunSynchronously + Assert.IsTrue (caughtGeneric, "Await of a cancelled Task<'T> should raise a TaskCanceledException catchable by try/with") + + let caughtNonGeneric = + async { + try + do! Async.Await (Task.FromCanceled ct) + return false + with :? TaskCanceledException -> return true } + |> Async.RunSynchronously + Assert.IsTrue (caughtNonGeneric, "Await of a cancelled Task should raise a TaskCanceledException catchable by try/with") + // This module contains tests for ComputationExpression not covered by the below TaskBuilderTests module module ComputationExpressionTests =