Error Handling & Fault Tolerance
In asynchronous streaming pipelines, handling runtime exceptions gracefully without losing context or leaving orphaned background tasks is vital. DataflowBuilder integrates fault-isolation and cancellation throughout the pipeline lifecycle.
1. Exception Trapping & Pipeline Faulting
In DataflowBuilder:
- Every action and transformation block wraps user delegates in safe error-trapping handlers (
HandleAsyncAction,HandleAsyncFunction, and try/catch blocks). - If an exception is thrown inside a transformation or target delegate:
- The exception is intercepted.
- The pipeline's ingress block (
SourceBlock) is immediately notified and marked faulted viaSourceBlock.Fault(exception). - The current block is faulted, halting further item consumption.
- The error propagates to the pipeline's completion task.
flowchart LR
Source[Source Block] --> Transform[Transform Block]
Transform -->|Exception Thrown| FaultedStage[Faulted Target Block]
FaultedStage -.->|SourceBlock.Fault| Source
FaultedStage -->|Awaited by| CompleteAsync[pipeline.CompleteAsync throws Exception]
2. Catching Errors on Completion
When an exception occurs inside a stage, pipeline.CompleteAsync() will re-throw the faulting exception:
var pipeline = DataFlowPipelineBuilder.FromSource<int>()
.Process(value =>
{
if (value == 0)
{
throw new InvalidOperationException("Division by zero error!");
}
return 100 / value;
})
.ToTarget(result => Console.WriteLine(result))
.Build();
try
{
await pipeline.SendAsync(10);
await pipeline.SendAsync(0); // Triggers fault
await pipeline.SendAsync(5);
await pipeline.CompleteAsync();
}
catch (InvalidOperationException ex)
{
Console.Error.WriteLine($"Pipeline failed cleanly: {ex.Message}");
}
3. Cooperative Cancellation with CancellationToken
Pipelines can be bound to a CancellationToken at initialization to support aborting long-running workflows:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var pipeline = DataFlowPipelineBuilder.FromSource<string>(cancellationToken: cts.Token)
.ProcessAsync(async item =>
{
return await LongRunningService.ProcessAsync(item, cts.Token);
})
.ToTarget(item => Console.WriteLine(item))
.Build();
try
{
await pipeline.SendAsync("Record 1", cts.Token);
await pipeline.CompleteAsync();
}
catch (OperationCanceledException)
{
Console.WriteLine("Pipeline processing cancelled by token.");
}
When a cancellation token is canceled:
- Entry buffers cease accepting new messages.
- Active blocks cancel their in-flight tasks where cancellation tokens are passed.
CompleteAsync()will resolve with anOperationCanceledExceptionorTaskCanceledException.
4. Best Practices for Error Resilience
Catch Transient Errors Locally: If individual items can fail and should be retried or routed to a dead-letter queue, handle that exception inside the delegate rather than letting it fault the entire pipeline:
.ProcessAsync(async item => { try { return await RemoteApi.SendAsync(item); } catch (HttpRequestException ex) { Logger.LogWarning(ex, "Failed item {Id}, routing to dead letter", item.Id); await DeadLetterQueue.PublishAsync(item); return null; // or fallback } })Always Call
CompleteAsyncin afinallyblock or ensure clean awaits: This prevents hung buffers from holding system resources.