StructuredConcurrency 0.2.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package StructuredConcurrency --version 0.2.1
                    
NuGet\Install-Package StructuredConcurrency -Version 0.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="StructuredConcurrency" Version="0.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="StructuredConcurrency" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="StructuredConcurrency" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add StructuredConcurrency --version 0.2.1
                    
#r "nuget: StructuredConcurrency, 0.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package StructuredConcurrency@0.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=StructuredConcurrency&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=StructuredConcurrency&version=0.2.1
                    
Install as a Cake Tool

StructuredConcurrency: TaskGroup and Promise.all / Promise.any for C# and .NET

NuGet Downloads CI License: MIT

Structured concurrency for C#. Run tasks in parallel inside a TaskGroup. The first failure cancels the other tasks, no task keeps running after the group returns, and you get the real error back instead of a TaskCanceledException. It includes C# versions of JavaScript's Promise.all, Promise.any and Promise.allSettled, and the same model as Python's asyncio.TaskGroup, Kotlin's coroutineScope, Swift's task groups, Java's StructuredTaskScope and Go's errgroup.

dotnet add package StructuredConcurrency

Promise.all, Promise.any and Promise.allSettled in C#

If you know JavaScript promises, you already know the API. Unlike JavaScript, the tasks that are no longer needed are actually cancelled.

Promise.all → TaskGroup.AllAsync. Run tasks in parallel and get all results; the first failure cancels the rest.

// JavaScript
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);
// C#
var (user, orders) = await TaskGroup.AllAsync(
    ct => GetUserAsync(id, ct),
    ct => GetOrdersAsync(id, ct));

Promise.any → TaskGroup.AnyAsync. Take the first task that succeeds; the others are cancelled.

// JavaScript
const data = await Promise.any(mirrors.map(m => m.download(path)));
// C#
string data = await TaskGroup.AnyAsync(mirrors, (mirror, ct) => mirror.DownloadAsync(path, ct));

Promise.allSettled → TaskGroup.AllSettledAsync. Run every task to completion and look at each outcome.

// JavaScript
const results = await Promise.allSettled(orders.map(o => charge(o)));
// C#
TaskOutcome<Receipt>[] results = await TaskGroup.AllSettledAsync(orders, (order, ct) => ChargeAsync(order, ct));
JavaScript StructuredConcurrency Built-in .NET, and what it doesn't do
Promise.all TaskGroup.AllAsync Task.WhenAll doesn't cancel the other tasks when one fails
Promise.any TaskGroup.AnyAsync No equivalent. Task.WhenAny returns the first task to complete, even if it failed
Promise.allSettled TaskGroup.AllSettledAsync Task.WhenAll, then inspect each task by hand
Promise.race Not needed Task.WhenAny

Why: the bugs in Task.WhenAll and Task.WhenAny code

Take a request that calls three services in parallel. One fails after 300 ms, and the other two take 1.5 s and 2 s:

Approach Caller gets the error at Error the caller sees Work left running
Task.WhenAll 2,012 ms OrderService is down None, but 1.7 s of calls whose results are discarded
await one by one 301 ms OrderService is down 2 calls keep running with nobody waiting
Hand-written CancellationTokenSource 310 ms A task was canceled. None
TaskGroup 320 ms OrderService is down None

The hand-written version cancels correctly but reports the wrong error: it awaits a task that was cancelled because another one failed. See samples/BeforeAfter.

Task.WhenAny has the opposite problem. With three mirrors, where A fails after 100 ms, B succeeds after 300 ms and C succeeds after 2 s:

Approach Caller gets At Work left running
Task.WhenAny A's error, even though B would succeed 104 ms B and C, with nobody waiting
Task.WhenEach + skip failures B's data 301 ms C, with nobody waiting
TaskGroup.AnyAsync B's data 334 ms None: C is cancelled

See samples/AnyVsWhenAny.

TaskGroup: structured concurrency in C#

For anything beyond the three helpers, open a group and start children in it:

using StructuredConcurrency;

await TaskGroup.RunAsync(async group =>
{
    foreach (var url in urls)
    {
        group.Run(ct => DownloadAsync(url, ct));
    }
}, cancellationToken);
// Every download has finished here, or the first failure was thrown after the rest were cancelled.

Each child receives the group's CancellationToken. Children can start more children, and groups can be nested.

Coming from another language

Language API In C#
Python 3.11+ asyncio.TaskGroup TaskGroup.RunAsync
Kotlin coroutineScope { } TaskGroup.RunAsync
Kotlin supervisorScope { } TaskGroup.RunAsync with new TaskGroupOptions { CancelOnFailure = false }
Swift withThrowingTaskGroup TaskGroup.RunAsync
Java 21+ StructuredTaskScope TaskGroup.RunAsync, TaskGroup.AnyAsync
Go errgroup.WithContext TaskGroup.RunAsync, TaskGroup.AllAsync

Guarantees

  1. Scope. RunAsync completes only after the body and every child have completed, even when the body throws.
  2. Failure. The first real failure cancels the group's token, which every child receives.
  3. Cancellation flows down. The group's token is linked to the caller's token, and a nested group is linked to its parent's.
  4. Errors surface as await expects. await rethrows the first failure. The returned task's Exception holds every failure in the order they occurred.
  5. Cancellations caused by the group are not failures. An OperationCanceledException thrown after the group was cancelled is dropped, so you see why the group stopped. A child's own cancellation, such as an HttpClient timeout, is reported as a failure.

FAQ

How do I cancel the other tasks when one task fails in C#?

Use TaskGroup.AllAsync for a fixed set of tasks, or TaskGroup.RunAsync for a dynamic one. The first exception cancels the shared CancellationToken, and the call waits for the cancelled tasks to stop before it rethrows the original exception. Task.WhenAll can't do this, because it has no way to cancel tasks.

What is the C# equivalent of Promise.any?

TaskGroup.AnyAsync. It returns the first successful result and cancels the rest. If every task fails, it throws an AggregateException with all the failures, the same as JavaScript's AggregateError. Task.WhenAny isn't an equivalent: it is Promise.race.

Why does Task.WhenAll wait for all tasks after one has failed?

Task.WhenAll only observes tasks; it can't stop them. The tasks keep running until they finish, so the caller waits for work whose results are discarded. Pass the group's token to every task with TaskGroup and they stop at the first failure.

Can't I do this with a CancellationTokenSource?

Yes, and TaskGroup uses one internally. Doing it by hand means cancelling on every failure, awaiting every task in a finally, and filtering out the OperationCanceledExceptions you caused. The table above shows the most common hand-written version reporting "A task was canceled." instead of the real error.

Does it run tasks on the thread pool?

No. group.Run invokes the delegate on the calling thread, like calling an async method. Wrap CPU-bound work in Task.Run.

How do I stop the remaining tasks once I have the result I need?

Call group.Cancel(). It cancels the remaining children without counting as a failure.

Performance

The benchmark compares TaskGroup with the code it replaces: a hand-written CancellationTokenSource that the first failure cancels, plus Task.WhenAll. It was run with BenchmarkDotNet on .NET 10 on an Apple M2 Pro; the source is in benchmarks.

Children Children complete Hand-written TaskGroup Hand-written alloc. TaskGroup alloc.
2 asynchronously 2.07 μs 2.06 μs 850 B 638 B
10 asynchronously 6.89 μs 7.18 μs 2,710 B 2,277 B
100 asynchronously 41.0 μs 43.9 μs 25,334 B 20,918 B
2 synchronously 49 ns 66 ns 160 B 224 B
10 synchronously 98 ns 152 ns 224 B 224 B
100 synchronously 876 ns 1,231 ns 944 B 224 B

When children do real asynchronous work, TaskGroup runs within the benchmark's noise (about ±10%) of the hand-written code and allocates 16–25% less. When children complete synchronously, it costs about 3.5 ns more per child, but its allocation stays at 224 B regardless of the number of children. AnyAsync over three operations takes 3.6 μs, against 4.1 μs for a hand-written Task.WhenEach loop, but allocates 1,840 B against 931 B.

Status

Version 0.2 includes TaskGroup.RunAsync, Run, Cancel, TaskGroupOptions, AllAsync, AnyAsync, AllSettledAsync and TaskOutcome<T>. The API may change before 1.0.

Targets .NET 8 and later, and is trimming and NativeAOT compatible.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on StructuredConcurrency:

Package Downloads
StructuredConcurrency.Hosting

A BackgroundService whose work runs in a StructuredConcurrency TaskGroup: on shutdown every child task is cancelled and awaited, and a failing child stops the service with its real exception instead of leaving work running in the background, or restarts the service's work as a RestartPolicy allows.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.8.0 101 10/23/2026
0.7.0 113 10/22/2026
0.6.0 102 10/22/2026
0.5.0 97 10/22/2026
0.4.0 89 10/22/2026
0.3.0 90 10/22/2026
0.2.1 77 10/22/2026
0.2.0 80 10/22/2026
0.1.0 81 10/21/2026