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:
Attempt 1: Immediate
Attempt 2: 500ms delay
Attempt 3: 1000ms delayOperations are transparently retried after reconnection succeeds. You don't need to implement reconnection logic in your application code.
Example:
// 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:
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:
[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 resumedOperation Timeout
All operations have a 10-second timeout to prevent indefinite hangs:
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:
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
catch (TimeoutException ex)
{
logger.LogWarning(ex, "Modbus operation timed out");
// Client will automatically retry on next operation
}SocketException - Network-level errors
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
catch (IOException ex)
{
logger.LogWarning(ex, "Connection lost during operation");
// Client will automatically reconnect and retry
}Protocol Errors
InvalidOperationException - Modbus protocol errors
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
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:
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
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:
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
| Layer | Purpose | Configuration |
|---|---|---|
| Retry | Recover from transient connection errors | 1 retry after reconnection |
| Circuit Breaker | Prevent hammering unavailable devices | Opens at 50% failure rate, 30s break |
| Timeout | Prevent indefinite hangs | 10 seconds per operation |
| Reconnection | Restore lost connections | Exponential backoff, 3 attempts max |
All layers work together automatically-no manual configuration required for typical use cases.