Skip to content
Draft
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
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Release notes:

0.6.0
- fixes: async { for item in taskSeq do ... } no longer wraps exceptions in AggregateException, #129
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
- adds TaskSeq.pairwise, #289
- adds TaskSeq.groupBy and TaskSeq.groupByAsync, #289
Expand Down
39 changes: 39 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module TaskSeq.Tests.AsyncExtensions

open System
open Xunit
open FsUnit.Xunit

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

module ExceptionPropagation =
[<Fact>]
let ``Async-for CE propagates exception without AggregateException wrapping`` () =
// Verifies fix for https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/129
// Async.AwaitTask previously wrapped all task exceptions in AggregateException,
// breaking try/catch blocks in async {} expressions that expect the original type.
let run () = async {
let values = taskSeq { yield 1 }

try
for _ in values do
raise (InvalidOperationException "test error")
with :? InvalidOperationException ->
()
}

// Should complete without AggregateException escaping
run () |> Async.RunSynchronously

[<Fact>]
let ``Async-for CE try-catch catches original exception type, not AggregateException`` () =
// Verifies that the original exception type is visible in catch blocks,
// not wrapped in AggregateException as Async.AwaitTask used to do.
let mutable caughtType: Type option = None

let run () = async {
let values = taskSeq { yield 1 }

try
for _ in values do
raise (ArgumentException "test")
with ex ->
caughtType <- Some(ex.GetType())
}

run () |> Async.RunSynchronously
caughtType |> should equal (Some typeof<ArgumentException>)

module Other =
[<Fact>]
let ``Async-for CE must call dispose in empty taskSeq`` () = async {
Expand Down
22 changes: 21 additions & 1 deletion src/FSharp.Control.TaskSeq/AsyncExtensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,30 @@ namespace FSharp.Control
[<AutoOpen>]
module AsyncExtensions =

// Awaits a Task<unit> without wrapping exceptions in AggregateException.
// Async.AwaitTask wraps task exceptions in AggregateException, which breaks try/catch
// blocks in async {} expressions that expect the original exception type.
// See: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/129
let private awaitTaskCorrect (task: System.Threading.Tasks.Task<unit>) : Async<unit> =
Async.FromContinuations(fun (cont, econt, ccont) ->
task.ContinueWith(fun (t: System.Threading.Tasks.Task<unit>) ->
if t.IsFaulted then
let exn = t.Exception

if exn.InnerExceptions.Count = 1 then
econt exn.InnerExceptions.[0]
else
econt exn
elif t.IsCanceled then
ccont (System.OperationCanceledException "The operation was cancelled.")
else
cont ())
|> ignore)

// Add asynchronous for loop to the 'async' computation builder
type Microsoft.FSharp.Control.AsyncBuilder with

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