Skip to content

Commit bd4d8ec

Browse files
authored
Merge pull request #321 from fsprojects/repo-assist/fix-async-awaittask-129-2026-03-2c2c8da1e1b359c3
[Repo Assist] fix: async { for x in taskSeq } no longer wraps exceptions in AggregateException
2 parents 83b1340 + d0f0c56 commit bd4d8ec

File tree

3 files changed

+61
-1
lines changed

3 files changed

+61
-1
lines changed

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
0.6.0
5+
- fixes: async { for item in taskSeq do ... } no longer wraps exceptions in AggregateException, #129
56
- adds TaskSeq.compareWith and TaskSeq.compareWithAsync
67
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
78
- adds TaskSeq.pairwise, #289

src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module TaskSeq.Tests.AsyncExtensions
22

3+
open System
34
open Xunit
45
open FsUnit.Xunit
56

@@ -115,6 +116,44 @@ module SideEffects =
115116
sum |> should equal 465 // eq to: List.sum [1..30]
116117
}
117118

119+
module ExceptionPropagation =
120+
[<Fact>]
121+
let ``Async-for CE propagates exception without AggregateException wrapping`` () =
122+
// Verifies fix for https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/129
123+
// Async.AwaitTask previously wrapped all task exceptions in AggregateException,
124+
// breaking try/catch blocks in async {} expressions that expect the original type.
125+
let run () = async {
126+
let values = taskSeq { yield 1 }
127+
128+
try
129+
for _ in values do
130+
raise (InvalidOperationException "test error")
131+
with :? InvalidOperationException ->
132+
()
133+
}
134+
135+
// Should complete without AggregateException escaping
136+
run () |> Async.RunSynchronously
137+
138+
[<Fact>]
139+
let ``Async-for CE try-catch catches original exception type, not AggregateException`` () =
140+
// Verifies that the original exception type is visible in catch blocks,
141+
// not wrapped in AggregateException as Async.AwaitTask used to do.
142+
let mutable caughtType: Type option = None
143+
144+
let run () = async {
145+
let values = taskSeq { yield 1 }
146+
147+
try
148+
for _ in values do
149+
raise (ArgumentException "test")
150+
with ex ->
151+
caughtType <- Some(ex.GetType())
152+
}
153+
154+
run () |> Async.RunSynchronously
155+
caughtType |> should equal (Some typeof<ArgumentException>)
156+
118157
module Other =
119158
[<Fact>]
120159
let ``Async-for CE must call dispose in empty taskSeq`` () = async {

src/FSharp.Control.TaskSeq/AsyncExtensions.fs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,30 @@ namespace FSharp.Control
33
[<AutoOpen>]
44
module AsyncExtensions =
55

6+
// Awaits a Task<unit> without wrapping exceptions in AggregateException.
7+
// Async.AwaitTask wraps task exceptions in AggregateException, which breaks try/catch
8+
// blocks in async {} expressions that expect the original exception type.
9+
// See: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/129
10+
let private awaitTaskCorrect (task: System.Threading.Tasks.Task<unit>) : Async<unit> =
11+
Async.FromContinuations(fun (cont, econt, ccont) ->
12+
task.ContinueWith(fun (t: System.Threading.Tasks.Task<unit>) ->
13+
if t.IsFaulted then
14+
let exn = t.Exception
15+
16+
if exn.InnerExceptions.Count = 1 then
17+
econt exn.InnerExceptions.[0]
18+
else
19+
econt exn
20+
elif t.IsCanceled then
21+
ccont (System.OperationCanceledException "The operation was cancelled.")
22+
else
23+
cont ())
24+
|> ignore)
25+
626
// Add asynchronous for loop to the 'async' computation builder
727
type Microsoft.FSharp.Control.AsyncBuilder with
828

929
member _.For(source: TaskSeq<'T>, action: 'T -> Async<unit>) =
1030
source
1131
|> TaskSeq.iterAsync (action >> Async.StartImmediateAsTask)
12-
|> Async.AwaitTask
32+
|> awaitTaskCorrect

0 commit comments

Comments
 (0)