From b1f792f64e2fd336ffce3227a1fb38ad35d5ef83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 07:22:35 +0000 Subject: [PATCH 1/2] fix: async { for x in taskSeq } no longer wraps exceptions in AggregateException Replace Async.AwaitTask with a correct awaiting function that unwraps single inner exceptions from AggregateException, so that try/catch blocks in async {} expressions see the original exception type. Also adds two regression tests that verify the fix: - exceptions propagate without AggregateException wrapping - catch blocks see the original exception type Closes #129 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- release-notes.txt | 1 + .../TaskSeq.AsyncExtensions.Tests.fs | 39 +++++++++++++++++++ src/FSharp.Control.TaskSeq/AsyncExtensions.fs | 22 ++++++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/release-notes.txt b/release-notes.txt index 5c0b97eb..dd295140 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -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 diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs index 1ccb781d..01ca72b5 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs @@ -1,5 +1,6 @@ module TaskSeq.Tests.AsyncExtensions +open System open Xunit open FsUnit.Xunit @@ -115,6 +116,44 @@ module SideEffects = sum |> should equal 465 // eq to: List.sum [1..30] } +module ExceptionPropagation = + [] + 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 + + [] + 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) + module Other = [] let ``Async-for CE must call dispose in empty taskSeq`` () = async { diff --git a/src/FSharp.Control.TaskSeq/AsyncExtensions.fs b/src/FSharp.Control.TaskSeq/AsyncExtensions.fs index 261e3437..d257d7de 100644 --- a/src/FSharp.Control.TaskSeq/AsyncExtensions.fs +++ b/src/FSharp.Control.TaskSeq/AsyncExtensions.fs @@ -3,10 +3,30 @@ namespace FSharp.Control [] module AsyncExtensions = + // Awaits a Task 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) : Async = + Async.FromContinuations(fun (cont, econt, ccont) -> + task.ContinueWith(fun (t: System.Threading.Tasks.Task) -> + 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) = source |> TaskSeq.iterAsync (action >> Async.StartImmediateAsTask) - |> Async.AwaitTask + |> awaitTaskCorrect From baa0ab2b48976bcd30c76a4e9fef6c1059272763 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 07:28:17 +0000 Subject: [PATCH 2/2] ci: trigger checks