High-Performance .NET: Async, Multithreading, and Parallel Programming Parallel Loops in .NET Created: 19 Jan 2026 Updated: 19 Jan 2026

Error Handling in Parallel Loops

When scaling your application using parallel loops, error handling becomes more complex than in a single-threaded foreach. Since multiple iterations are running concurrently on different threads, an exception on one thread doesn't immediately kill the others, but it does change how the loop behaves.

Based on insights from Jeff McNamara’s Ultimate C# for High-Performance Applications, here is a guide on managing exceptions effectively to build robust parallel systems.

1. The AggregateException Concept

When an unhandled exception occurs inside a parallel loop, the framework prevents new iterations from starting. However, it allows already running iterations to finish. Because multiple errors might occur simultaneously across different threads, the .NET runtime wraps all these individual errors into a single AggregateException.

You can access the specific errors by inspecting the InnerExceptions property of the AggregateException.

2. Best Practices for Error Handling

To maintain high performance and reliability, follow these industry best practices:

  1. Handle Within the Body: Whenever possible, use a try-catch block inside the loop body. This allows you to log the specific data that caused the error and decide whether to continue or stop.
  2. Check the Result: Use the ParallelLoopResult returned by the loop. Its IsComplete property tells you if everything finished successfully.
  3. Avoid Nesting: Never nest parallel loops (putting a Parallel.For inside another). This can cause an explosion in thread count, overwhelming the system and making error tracking a nightmare.
  4. Graceful Exits: If an exception occurs, use state.Stop() or state.Break() within your catch block to prevent unnecessary work on other threads.

3. Practical Example: Safe Parallel Data Processing

In this example, we process a list of data. We handle exceptions inside the loop to log specific failures, and we use a global try-catch to catch the aggregated results.

var errorHandlingDemo = new ErrorHandlingDemo();

var dataItems = new List<string> { "Item1", "ERROR", "Item3", "ERROR", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10", };
errorHandlingDemo.ProcessData(dataItems);

public class ErrorHandlingDemo
{
public void ProcessData(List<string> dataItems)
{
try
{
ParallelLoopResult result = Parallel.ForEach(dataItems, (item, state) =>
{
try
{
//random sleep to simulate processing time
Thread.Sleep(new Random().Next(1000, 5000));
// Simulated logic that might fail
if (item == "ERROR")
throw new ArgumentException($"Invalid item: {item}");

Console.WriteLine($"Successfully processed: {item}");
}
catch (Exception ex)
{
// Handle individually to log specific context
Console.WriteLine($"[Local Catch] Error processing '{item}': {ex.Message}");
// Stop further processing if one item fails
state.Stop();
}
});

if (!result.IsCompleted)
{
Console.WriteLine($"Loop interrupted");
}
}
catch (AggregateException ae)
{
// Catch any exceptions that escaped the internal try-catch
foreach (var inner in ae.InnerExceptions)
{
Console.WriteLine($"[Aggregate Catch] {inner.Message}");
}
}
}
public void ProcessDataWithAggregate(List<string> dataItems)
{
try
{
ParallelLoopResult result = Parallel.ForEach(dataItems, (item, state) =>
{
//random sleep to simulate processing time
Thread.Sleep(new Random().Next(1000, 5000));
// Simulated logic that might fail
if (item == "ERROR")
throw new ArgumentException($"Invalid item: {item}");

Console.WriteLine($"Successfully processed: {item}");
});

if (!result.IsCompleted)
{
Console.WriteLine($"Loop interrupted. Lowest break index: {result.LowestBreakIteration}");
}
}
catch (AggregateException ae)
{
// Catch any exceptions that escaped the internal try-catch
foreach (var inner in ae.InnerExceptions)
{
Console.WriteLine($"[Aggregate Catch] {inner.Message}");
}
}
}
}

Summary Table: Parallel Exception Strategies

StrategyWhen to UseBenefit
Local Try-CatchMost scenarios.Allows logging of specific data and "retrying" logic.
AggregateExceptionGlobal safety net.Catches all errors from all threads in one place.
ParallelLoopResultPost-loop check.Quickly verify if the loop finished or was interrupted.


Share this lesson: