Skip to content

Resilience and Error Handling

The Modbus client includes a multi-layered resilience pipeline designed for production environments with unreliable networks or devices.

Automatic Reconnection

When a connection is lost, the client automatically attempts to reconnect with exponential backoff:

text
Attempt 1: Immediate
Attempt 2: 500ms delay
Attempt 3: 1000ms delay

Operations are transparently retried after reconnection succeeds. You don't need to implement reconnection logic in your application code.

Example:

csharp
// If the connection drops during this operation:
// 1. Client detects the connection error
// 2. Initiates automatic reconnection
// 3. Retries the operation after reconnecting
var value = await client.ReadHoldingRegistersAsync<float>(0, 1);

Retry Strategy

Failed operations due to connection errors are automatically retried once after successful reconnection:

csharp
try
{
    // This operation will automatically retry once if connection is lost
    var voltage = await client.ReadHoldingRegistersAsync<float>(0, 1);
}
catch (IOException ex)
{
    // Only thrown if reconnection failed or retry also failed
    logger.LogError(ex, "Failed to read voltage after retry");
}

Retry behavior:

  • Transient connection errors: Automatic retry after reconnection
  • Non-connection errors: No retry (e.g., invalid register address)
  • Retry count: 1 (total 2 attempts)

Circuit Breaker

The circuit breaker protects against repeatedly hammering unavailable devices:

States

Closed - Normal operation, requests flow through

Open - Fast-fail mode, requests fail immediately without attempting communication

Half-Open - Testing recovery, allowing one request through to check if the device is back online

Thresholds

  • Opens after: 50% failure rate over 3+ requests in 10 seconds
  • Stays open for: 30 seconds
  • Half-open test: Single request to verify recovery

Example log output:

text
[Warning] Circuit breaker opened for target=192.168.1.100, break_duration=30s
[Info] Circuit breaker half-open for target=192.168.1.100, testing recovery
[Info] Circuit breaker closed for target=192.168.1.100, normal operation resumed

Operation Timeout

All operations have a 10-second timeout to prevent indefinite hangs:

csharp
try
{
    // Automatically times out after 10 seconds
    var value = await client.ReadHoldingRegistersAsync<float>(0, 1);
}
catch (TimeoutException)
{
    // Operation took too long
}

You can implement custom timeouts using CancellationToken:

csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

try
{
    var value = await client.ReadHoldingRegistersAsync<float>(0, 1);
}
catch (OperationCanceledException)
{
    // Custom 5-second timeout exceeded
}

Exception Types

Understanding exception types helps you implement appropriate error handling:

Connection Errors

TimeoutException - Connection establishment or operation timeout

csharp
catch (TimeoutException ex)
{
    logger.LogWarning(ex, "Modbus operation timed out");
    // Client will automatically retry on next operation
}

SocketException - Network-level errors

csharp
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
    logger.LogError(ex, "Device refused connection, check firewall/port");
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.HostUnreachable)
{
    logger.LogError(ex, "Device unreachable, check network/IP");
}

IOException - TCP connection lost during operation

csharp
catch (IOException ex)
{
    logger.LogWarning(ex, "Connection lost during operation");
    // Client will automatically reconnect and retry
}

Protocol Errors

InvalidOperationException - Modbus protocol errors

csharp
catch (InvalidOperationException ex) when (ex.Message.Contains("Illegal data address"))
{
    logger.LogError(ex, "Invalid register address");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("Slave device failure"))
{
    logger.LogError(ex, "Device reported internal error");
}

Common Modbus exception codes:

  • Illegal Function (01): Operation not supported by device
  • Illegal Data Address (02): Register address out of range
  • Illegal Data Value (03): Invalid value for write operation
  • Slave Device Failure (04): Device internal error

Best Practices

1. Handle Expected Exceptions

csharp
public async Task<float?> ReadVoltageAsync(IVoltimaxModbusClient client)
{
    try
    {
        return await client.ReadHoldingRegistersAsync<float>(0, 1);
    }
    catch (TimeoutException ex)
    {
        logger.LogWarning(ex, "Voltage read timeout");
        return null; // Client will auto-recover
    }
    catch (IOException ex)
    {
        logger.LogWarning(ex, "Connection lost during voltage read");
        return null; // Client will auto-recover
    }
    catch (InvalidOperationException ex)
    {
        logger.LogError(ex, "Invalid register or device error");
        throw; // Likely configuration issue
    }
}

2. Implement Application-Level Retry

For critical operations, add application-level retry with exponential backoff:

csharp
var retryPolicy = Policy
    .Handle<TimeoutException>()
    .Or<IOException>()
    .WaitAndRetryAsync(3, 
        retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
        onRetry: (ex, timespan, retryCount, context) =>
        {
            logger.LogWarning(ex, "Retry {RetryCount} after {Delay}s", 
                retryCount, timespan.TotalSeconds);
        });

var voltage = await retryPolicy.ExecuteAsync(() => 
    client.ReadHoldingRegistersAsync<float>(0, 1));

3. Monitor Connection State

csharp
public class DeviceMonitor : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            if (!client.IsConnected)
            {
                logger.LogWarning("Modbus client disconnected");
                // Optional: Alert monitoring system
            }
            
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

4. Use Cancellation Tokens

Always pass cancellation tokens for graceful shutdown:

csharp
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        try
        {
            var value = await client.ReadHoldingRegistersAsync<float>(0, 1);
            // Process value...
        }
        catch (OperationCanceledException)
        {
            // Shutdown requested
            break;
        }
        
        await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
    }
}

Resilience Pipeline Summary

LayerPurposeConfiguration
RetryRecover from transient connection errors1 retry after reconnection
Circuit BreakerPrevent hammering unavailable devicesOpens at 50% failure rate, 30s break
TimeoutPrevent indefinite hangs10 seconds per operation
ReconnectionRestore lost connectionsExponential backoff, 3 attempts max

All layers work together automatically-no manual configuration required for typical use cases.