Error Handling
Belay.NET provides comprehensive error handling to help you build robust IoT applications. Learn how to handle device connection issues, execution errors, and implement proper error recovery patterns.
Exception Hierarchy
Belay.NET uses a structured exception hierarchy derived from BelayException
:
BelayException
├── DeviceConnectionException // Connection and communication failures
├── DeviceExecutionException // Python code execution errors
├── DeviceTimeoutException // Operation timeout errors
├── DeviceConfigurationException // Configuration and setup errors
└── DeviceStateException // Invalid device state errors
Connection Error Handling
Handle device connection failures gracefully:
using Belay.Core;
using Belay.Core.Communication;
using Belay.Core.Exceptions;
public async Task<bool> TryConnectAsync(string port)
{
try
{
using var device = new Device(new SerialDeviceCommunication(port));
await device.StartAsync();
Console.WriteLine($"✅ Connected to device on {port}");
return true;
}
catch (DeviceConnectionException ex)
{
Console.WriteLine($"❌ Connection failed: {ex.Message}");
// Check specific connection failure reasons
switch (ex.Reason)
{
case ConnectionFailureReason.PortNotFound:
Console.WriteLine($"💡 Port {port} not found. Available ports:");
foreach (var availablePort in SerialPort.GetPortNames())
Console.WriteLine($" - {availablePort}");
break;
case ConnectionFailureReason.PortInUse:
Console.WriteLine("💡 Port is already in use by another application");
break;
case ConnectionFailureReason.AccessDenied:
Console.WriteLine("💡 Access denied. Try running as administrator or check permissions");
break;
case ConnectionFailureReason.DeviceNotResponding:
Console.WriteLine("💡 Device not responding. Check firmware and USB connection");
break;
}
return false;
}
}
Execution Error Handling
Handle Python code execution errors with detailed context:
public async Task<float> ReadSensorWithErrorHandlingAsync()
{
try
{
return await _device.ExecuteAsync<float>(@"
# This might fail if sensor is not connected
sensor_value = adc.read_u16()
if sensor_value == 0:
raise ValueError('Sensor not connected')
temperature = sensor_value * 3.3 / 65536 * 100
temperature
");
}
catch (DeviceExecutionException ex)
{
Console.WriteLine($"❌ Python execution error: {ex.Message}");
Console.WriteLine($"🐍 Exception type: {ex.PythonExceptionType}");
Console.WriteLine($"📍 Line number: {ex.LineNumber}");
Console.WriteLine($"📋 Traceback:\n{ex.PythonTraceback}");
// Handle specific Python exception types
switch (ex.PythonExceptionType)
{
case "ValueError":
Console.WriteLine("💡 Check sensor connections and wiring");
return float.NaN; // Return sentinel value
case "AttributeError":
Console.WriteLine("💡 Check that required modules are imported");
throw; // Re-throw as this is a code error
case "OSError":
Console.WriteLine("💡 Hardware error - check device connections");
return float.NaN;
default:
Console.WriteLine("💡 Unexpected error - check Python code");
throw; // Re-throw unknown errors
}
}
}
Timeout Handling
Handle operation timeouts with retry logic:
public async Task<T> ExecuteWithRetryAsync<T>(string code, int maxAttempts = 3, TimeSpan? timeout = null)
{
var actualTimeout = timeout ?? TimeSpan.FromSeconds(30);
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
using var cts = new CancellationTokenSource(actualTimeout);
return await _device.ExecuteAsync<T>(code, cts.Token);
}
catch (DeviceTimeoutException ex) when (attempt < maxAttempts)
{
Console.WriteLine($"⏱️ Timeout on attempt {attempt}/{maxAttempts}: {ex.Message}");
Console.WriteLine($"🔄 Retrying in {attempt * 1000}ms...");
await Task.Delay(attempt * 1000); // Exponential backoff
}
catch (DeviceTimeoutException ex) when (attempt == maxAttempts)
{
Console.WriteLine($"❌ All {maxAttempts} attempts timed out");
throw new DeviceExecutionException(
$"Operation failed after {maxAttempts} attempts", ex);
}
}
throw new InvalidOperationException("This should never be reached");
}
State Management Errors
Handle device state errors with automatic recovery:
public class ResilientDevice : IDisposable
{
private Device _device;
private readonly string _port;
private volatile bool _isConnected;
public ResilientDevice(string port)
{
_port = port;
}
public async Task<T> ExecuteAsync<T>(string code)
{
const int maxReconnectAttempts = 3;
for (int attempt = 1; attempt <= maxReconnectAttempts; attempt++)
{
try
{
await EnsureConnectedAsync();
return await _device.ExecuteAsync<T>(code);
}
catch (DeviceStateException ex) when (attempt < maxReconnectAttempts)
{
Console.WriteLine($"🔄 Device state error, reconnecting (attempt {attempt}/{maxReconnectAttempts}): {ex.Message}");
// Force reconnection
await DisconnectAsync();
await Task.Delay(1000 * attempt); // Progressive delay
}
}
throw new DeviceExecutionException($"Failed to execute after {maxReconnectAttempts} reconnection attempts");
}
private async Task EnsureConnectedAsync()
{
if (_device == null || !_isConnected)
{
await ConnectAsync();
}
}
private async Task ConnectAsync()
{
try
{
_device?.Dispose();
_device = new Device(new SerialDeviceCommunication(_port));
await _device.StartAsync();
_isConnected = true;
Console.WriteLine("✅ Device connected successfully");
}
catch (Exception)
{
_isConnected = false;
throw;
}
}
private async Task DisconnectAsync()
{
if (_device != null)
{
try
{
await _device.StopAsync();
}
catch
{
// Ignore errors during disconnection
}
finally
{
_device.Dispose();
_device = null;
_isConnected = false;
}
}
}
public void Dispose()
{
DisconnectAsync().GetAwaiter().GetResult();
}
}
Global Error Handling
Implement application-wide error handling:
public class DeviceErrorHandler
{
private readonly ILogger<DeviceErrorHandler> _logger;
private readonly IDeviceHealthMonitor _healthMonitor;
public DeviceErrorHandler(ILogger<DeviceErrorHandler> logger, IDeviceHealthMonitor healthMonitor)
{
_logger = logger;
_healthMonitor = healthMonitor;
}
public async Task<ExecutionResult<T>> SafeExecuteAsync<T>(
Device device,
string code,
string operationName = "Unknown")
{
try
{
var result = await device.ExecuteAsync<T>(code);
_logger.LogInformation("Operation {OperationName} completed successfully", operationName);
await _healthMonitor.RecordSuccessAsync(device.Id, operationName);
return ExecutionResult<T>.Success(result);
}
catch (DeviceConnectionException ex)
{
_logger.LogError(ex, "Connection error during {OperationName}", operationName);
await _healthMonitor.RecordFailureAsync(device.Id, operationName, ex);
return ExecutionResult<T>.Failure($"Connection error: {ex.Message}");
}
catch (DeviceExecutionException ex)
{
_logger.LogError(ex, "Execution error during {OperationName}: {PythonException} at line {LineNumber}",
operationName, ex.PythonExceptionType, ex.LineNumber);
await _healthMonitor.RecordFailureAsync(device.Id, operationName, ex);
return ExecutionResult<T>.Failure($"Execution error: {ex.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during {OperationName}", operationName);
await _healthMonitor.RecordFailureAsync(device.Id, operationName, ex);
return ExecutionResult<T>.Failure($"Unexpected error: {ex.Message}");
}
}
}
public class ExecutionResult<T>
{
public bool Success { get; }
public T? Value { get; }
public string? ErrorMessage { get; }
private ExecutionResult(bool success, T? value, string? errorMessage)
{
Success = success;
Value = value;
ErrorMessage = errorMessage;
}
public static ExecutionResult<T> Success(T value) => new(true, value, null);
public static ExecutionResult<T> Failure(string errorMessage) => new(false, default, errorMessage);
}
Error Recovery Patterns
Circuit Breaker Pattern
Prevent cascade failures with circuit breaker:
public class DeviceCircuitBreaker
{
private readonly int _failureThreshold;
private readonly TimeSpan _timeout;
private int _failureCount;
private DateTime _nextAttempt = DateTime.MinValue;
private CircuitState _state = CircuitState.Closed;
public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation)
{
if (_state == CircuitState.Open)
{
if (DateTime.UtcNow < _nextAttempt)
throw new DeviceStateException("Circuit breaker is open");
_state = CircuitState.HalfOpen;
}
try
{
var result = await operation();
OnSuccess();
return result;
}
catch (Exception)
{
OnFailure();
throw;
}
}
private void OnSuccess()
{
_failureCount = 0;
_state = CircuitState.Closed;
}
private void OnFailure()
{
_failureCount++;
if (_failureCount >= _failureThreshold)
{
_state = CircuitState.Open;
_nextAttempt = DateTime.UtcNow.Add(_timeout);
}
}
}
enum CircuitState { Closed, Open, HalfOpen }
Bulkhead Pattern
Isolate failures between different device operations:
public class DeviceOperationIsolator
{
private readonly SemaphoreSlim _sensorSemaphore = new(3); // Max 3 concurrent sensor operations
private readonly SemaphoreSlim _actuatorSemaphore = new(2); // Max 2 concurrent actuator operations
public async Task<T> ExecuteSensorOperationAsync<T>(Func<Task<T>> operation)
{
await _sensorSemaphore.WaitAsync();
try
{
return await operation();
}
finally
{
_sensorSemaphore.Release();
}
}
public async Task ExecuteActuatorOperationAsync(Func<Task> operation)
{
await _actuatorSemaphore.WaitAsync();
try
{
await operation();
}
finally
{
_actuatorSemaphore.Release();
}
}
}
Testing Error Scenarios
Write tests for error conditions:
[TestMethod]
public async Task Device_WhenPythonRaisesException_ShouldThrowDeviceExecutionException()
{
// Arrange
using var device = await Device.ConnectAsync("COM3");
// Act & Assert
var ex = await Assert.ThrowsExceptionAsync<DeviceExecutionException>(async () =>
{
await device.ExecuteAsync("raise ValueError('Test error')");
});
Assert.AreEqual("ValueError", ex.PythonExceptionType);
Assert.IsTrue(ex.Message.Contains("Test error"));
}
[TestMethod]
public async Task Device_WhenConnectionLost_ShouldThrowDeviceConnectionException()
{
// Arrange
var mockCommunication = new Mock<IDeviceCommunication>();
mockCommunication.Setup(c => c.SendAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Connection lost"));
using var device = new Device(mockCommunication.Object);
// Act & Assert
await Assert.ThrowsExceptionAsync<DeviceConnectionException>(async () =>
{
await device.ExecuteAsync("print('test')");
});
}
Best Practices
1. Always Use Using Statements
// ✅ Good - Ensures proper cleanup
using var device = new Device(communication);
await device.StartAsync();
// ❌ Bad - May leak resources
var device = new Device(communication);
await device.StartAsync();
// Missing disposal
2. Handle Specific Exception Types
// ✅ Good - Specific error handling
catch (DeviceConnectionException ex)
{
// Handle connection issues
}
catch (DeviceExecutionException ex)
{
// Handle Python execution errors
}
// ❌ Bad - Generic error handling
catch (Exception ex)
{
// Can't provide specific guidance to user
}
3. Log Errors with Context
// ✅ Good - Structured logging with context
_logger.LogError(ex, "Failed to read sensor {SensorType} on device {DeviceId}",
sensorType, deviceId);
// ❌ Bad - Generic error message
_logger.LogError(ex, "Error occurred");
4. Implement Graceful Degradation
public async Task<SensorReading> ReadSensorAsync()
{
try
{
var value = await _device.ExecuteAsync<float>("sensor.read()");
return new SensorReading(value, DateTime.UtcNow, SensorStatus.Normal);
}
catch (DeviceExecutionException)
{
// Graceful degradation - return stale data or estimated value
return new SensorReading(float.NaN, DateTime.UtcNow, SensorStatus.Error);
}
}
Monitoring and Alerting
Integrate with monitoring systems:
public class DeviceErrorReporter
{
private readonly IMetrics _metrics;
private readonly IAlertingService _alerting;
public async Task ReportErrorAsync(Device device, Exception error)
{
// Record metrics
_metrics.Counter("device_errors_total")
.WithTag("device_id", device.Id)
.WithTag("error_type", error.GetType().Name)
.Increment();
// Send alerts for critical errors
if (error is DeviceConnectionException && IsCriticalDevice(device))
{
await _alerting.SendAlertAsync(new Alert
{
Severity = AlertSeverity.Critical,
Message = $"Critical device {device.Id} connection lost",
Device = device.Id,
Timestamp = DateTime.UtcNow
});
}
}
}
Summary
Effective error handling in Belay.NET involves:
- Structured Exceptions: Use the exception hierarchy for specific error types
- Connection Resilience: Handle connection failures with retry and reconnection logic
- Execution Errors: Parse Python errors and provide meaningful feedback
- Recovery Patterns: Implement circuit breakers and bulkheads for fault tolerance
- Monitoring: Track errors and alert on critical failures
- Testing: Write tests for error scenarios to ensure robust applications
Next: Learn about Session Management for advanced device lifecycle handling.