Skip to content

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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
[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 ​

csharp
// ✅ 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 ​

csharp
// ✅ 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 ​

csharp
// ✅ 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 ​

csharp
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:

csharp
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.

Released under the Apache License 2.0.