Common Patterns

Error Handling Patterns

Train-Level Error Handling

public class RobustTrain : ServiceTrain<ProcessOrderRequest, ProcessOrderResult>
{
    protected override async Task<Either<Exception, ProcessOrderResult>> RunInternal(ProcessOrderRequest input)
    {
        try
        {
            return await Activate(input)
                .Chain<ValidateOrderJunction>()
                .Chain<ProcessPaymentJunction>()
                .Chain<FulfillOrderJunction>()
                .Resolve();
        }
        catch (PaymentException ex)
        {
            // Handle payment-specific errors
            Logger?.LogWarning("Payment failed for order {OrderId}: {Error}",
                input.OrderId, ex.Message);
            return new OrderProcessingException("Payment processing failed", ex);
        }
        catch (InventoryException ex)
        {
            // Handle inventory-specific errors
            return new OrderProcessingException("Insufficient inventory", ex);
        }
    }
}

Junction-Level Error Handling

public class RobustJunction(IPaymentGateway PaymentGateway) : Junction<PaymentRequest, PaymentResult>
{
    public override async Task<PaymentResult> Run(PaymentRequest input)
    {
        try
        {
            var result = await PaymentGateway.ProcessAsync(input);
            return result;
        }
        catch (TimeoutException ex)
        {
            // Throw a meaningful error
            throw new PaymentException("Payment gateway timed out", ex);
        }
    }
}

Cancellation Patterns

Passing Tokens from ASP.NET Controllers

ASP.NET Core provides a CancellationToken that fires when the HTTP request is aborted:

[HttpPost("orders")]
public async Task<IActionResult> CreateOrder(
    CreateOrderRequest request,
    CancellationToken cancellationToken)
{
    var result = await trainBus.RunAsync<OrderResult>(request, cancellationToken);
    return Ok(result);
}

Using the Token in Junctions

Access this.CancellationToken inside any junction to pass it to async operations:

public class QueryDatabaseJunction(IDataContext context) : Junction<UserId, User>
{
    public override async Task<User> Run(UserId input)
    {
        return await context.Users
            .FirstOrDefaultAsync(u => u.Id == input.Value, CancellationToken)
            ?? throw new NotFoundException($"User {input.Value} not found");
    }
}

Checking Cancellation in Long-Running Junctions

For junctions that iterate over large collections, check cancellation periodically:

public class BatchProcessJunction : Junction<BatchInput, BatchResult>
{
    public override async Task<BatchResult> Run(BatchInput input)
    {
        var results = new List<ItemResult>();
 
        foreach (var item in input.Items)
        {
            CancellationToken.ThrowIfCancellationRequested();
            results.Add(await ProcessItem(item));
        }
 
        return new BatchResult(results);
    }
}

Full details: Cancellation Tokens

SDK Reference

> Activate | Chain | Resolve | RunAsync