Skip to content

Commit 545729c

Browse files
Copilotwallymathieu
authored andcommitted
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>
1 parent 44ebc37 commit 545729c

3 files changed

Lines changed: 34 additions & 8 deletions

File tree

‎src/FSharpPlus/Extensions/Extensions.fs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,13 @@ module Extensions =
163163
/// at the point where the overall async is started.
164164
/// </remarks>
165165
static member Await (task: Task<'T>) : Async<'T> =
166-
Async.FromContinuations (fun (sc, ec, cc) ->
166+
Async.FromContinuations (fun (sc, ec, _) ->
167167
task.ContinueWith (fun (task: Task<'T>) ->
168168
if task.IsFaulted then
169169
let e = Unchecked.nonNull task.Exception
170170
if e.InnerExceptions.Count = 1 then ec e.InnerExceptions[0]
171171
else ec e
172-
elif task.IsCanceled then cc (TaskCanceledException ())
172+
elif task.IsCanceled then ec (TaskCanceledException ())
173173
else sc task.Result)
174174
|> ignore)
175175

@@ -190,13 +190,13 @@ module Extensions =
190190
/// at the point where the overall async is started.
191191
/// </remarks>
192192
static member Await (task: Task) : Async<unit> =
193-
Async.FromContinuations (fun (sc, ec, cc) ->
193+
Async.FromContinuations (fun (sc, ec, _) ->
194194
task.ContinueWith (fun (task: Task) ->
195195
if task.IsFaulted then
196196
let e = Unchecked.nonNull task.Exception
197197
if e.InnerExceptions.Count = 1 then ec e.InnerExceptions[0]
198198
else ec e
199-
elif task.IsCanceled then cc (TaskCanceledException ())
199+
elif task.IsCanceled then ec (TaskCanceledException ())
200200
else sc ())
201201
|> ignore)
202202

‎tests/FSharpPlus.Tests/Asyncs.fs‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ module Async =
4141
let t2 = createAsync true 0 2
4242
let t3 = createAsync true 0 3
4343

44-
let c = new CancellationToken true
45-
let t4 = Task.FromCanceled<int> c |> Async.Await
44+
// A genuinely cancelled async: Async.Await no longer surfaces a cancelled Task as a
45+
// cancellation, it raises TaskCanceledException (fslang-suggestions #840).
46+
let t4 : Async<int> = Async.FromContinuations (fun (_, _, cc) -> cc (OperationCanceledException ()))
4647

4748
let t5 = createAsync false 0 5
4849
let t6 = createAsync false 0 6
@@ -71,8 +72,9 @@ module Async =
7172
let t2 = createAsync true 10 2
7273
let t3 = createAsync true 30 3
7374

74-
let c = new CancellationToken true
75-
let t4 = Task.FromCanceled<int> c |> Async.Await
75+
// A genuinely cancelled async: Async.Await no longer surfaces a cancelled Task as a
76+
// cancellation, it raises TaskCanceledException (fslang-suggestions #840).
77+
let t4 : Async<int> = Async.FromContinuations (fun (_, _, cc) -> cc (OperationCanceledException ()))
7678

7779
let t5 = createAsync false 20 5
7880
let t6 = createAsync false 10 6

‎tests/FSharpPlus.Tests/Task.fs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,30 @@ module Task =
273273
Assert.AreEqual (e0, e1, "Original exception is not the same as that extracted from the Async")
274274
Assert.AreEqual (e1, e2, "The exception extracted from the Async is not the same as that extracted from the roundtripped Task")
275275

276+
[<Test>]
277+
let awaitOfCancelledTaskRaisesTaskCanceledException () =
278+
// A cancelled Task is surfaced through the exception continuation as a TaskCanceledException, so it can be
279+
// caught with an ordinary try/with around the Await, matching C# await (fslang-suggestions #840).
280+
let ct = CancellationToken true
281+
282+
let caughtGeneric =
283+
async {
284+
try
285+
let! _ = Async.Await (Task.FromCanceled<int> ct)
286+
return false
287+
with :? TaskCanceledException -> return true }
288+
|> Async.RunSynchronously
289+
Assert.IsTrue (caughtGeneric, "Await of a cancelled Task<'T> should raise a TaskCanceledException catchable by try/with")
290+
291+
let caughtNonGeneric =
292+
async {
293+
try
294+
do! Async.Await (Task.FromCanceled ct)
295+
return false
296+
with :? TaskCanceledException -> return true }
297+
|> Async.RunSynchronously
298+
Assert.IsTrue (caughtNonGeneric, "Await of a cancelled Task should raise a TaskCanceledException catchable by try/with")
299+
276300

277301
// This module contains tests for ComputationExpression not covered by the below TaskBuilderTests module
278302
module ComputationExpressionTests =

0 commit comments

Comments
 (0)