Exception Handling in IronLedger

This document describes the exception handling strategy used in the IronLedger library, including when to use custom exceptions vs. standard .NET exceptions.

Exception Hierarchy

IronLedger defines a custom exception hierarchy to provide clear, actionable error information:

System.Exception
└── IronLedgerException (base for all IronLedger errors)
    └── ComponentDataException (component data operations)
        └── ComponentDataProviderException (provider-specific failures)

Custom Exception Classes

IronLedgerException

Purpose: Base exception for all IronLedger library errors.

When to use: This is the root of the IronLedger exception hierarchy. Generally, you should throw more specific derived exceptions rather than this base class directly.

Example:

// Rarely used directly - prefer more specific exceptions
throw new IronLedgerException("An unexpected error occurred in IronLedger");

ComponentDataException

Purpose: Thrown when component data operations fail.

When to use:

  • General component data manipulation errors
  • Data validation failures specific to component data
  • Operations on ComponentData, AssetMetadata, or related types

Example:

throw new ComponentDataException("Failed to process component data");

ComponentDataProviderException

Purpose: Thrown when a component data provider fails to retrieve data from the underlying data source (WMI/CIM, etc.).

When to use:

  • WMI/CIM query failures
  • Hardware data retrieval errors
  • Provider initialization failures

Properties:

  • ProviderName: The name of the provider that failed (e.g., "ProcessorDataProvider")
  • WmiClassName: The WMI class being queried when the failure occurred (e.g., "Win32_Processor")

Example:

try
{
    // Query WMI
}
catch (Exception ex)
{
    throw new ComponentDataProviderException(
        $"Failed to retrieve data from WMI class '{WmiClassName}'.",
        ex)
    {
        ProviderName = GetType().Name,
        WmiClassName = WmiClassName
    };
}

Standard .NET Exceptions

IronLedger also uses standard .NET exceptions where appropriate. Use these instead of custom exceptions for common programming errors:

ArgumentNullException

When to use: A required parameter or argument is null.

Example:

public IronLedgerJsonSerializer(JsonSerializerOptions options)
{
    _options = options ?? throw new ArgumentNullException(nameof(options));
}

ArgumentException

When to use:

  • An argument value is invalid (but not null)
  • String arguments that are empty or whitespace
  • Arguments outside valid ranges

Example:

public T? Deserialize<T>(string data)
{
    if (string.IsNullOrWhiteSpace(data))
        throw new ArgumentException("Data cannot be null or whitespace.", nameof(data));
    
    // ...
}

InvalidOperationException

When to use:

  • An operation is invalid for the object's current state
  • Method calls in the wrong sequence
  • Operations attempted when the object is not properly initialized

Example:

if (!_isInitialized)
    throw new InvalidOperationException("Provider must be initialized before use");

Exception Handling Guidelines

1. When to Catch Exceptions

DO catch exceptions when:

  • You can handle the error meaningfully
  • You need to wrap lower-level exceptions with domain-specific exceptions
  • You need to add context before re-throwing
  • You need to log and recover gracefully

DON'T catch exceptions when:

  • You can't do anything meaningful with them
  • You're just going to re-throw without adding value
  • The calling code needs to handle the error

2. Exception Wrapping in Providers

All provider base classes (CimDataProviderBase, CimMetadataProviderBase) wrap low-level CIM/WMI exceptions:

public IReadOnlyList<ComponentData> GetData()
{
    try
    {
        // WMI query logic
    }
    catch (Exception ex) when (ex is not ComponentDataProviderException)
    {
        throw new ComponentDataProviderException(
            $"Failed to retrieve data from WMI class '{WmiClassName}'.",
            ex)
        {
            ProviderName = GetType().Name,
            WmiClassName = WmiClassName
        };
    }
}

Key points:

  • Only catch exceptions that are not already ComponentDataProviderException (using exception filter)
  • Always include the original exception as InnerException
  • Populate ProviderName and WmiClassName properties for diagnostics

3. Factory Methods and Exception Propagation

Factory classes (ComponentDataFactory, AssetIdFactory) do NOT catch provider exceptions - they let them propagate:

public SystemComponentData Create()
{
    // These calls may throw ComponentDataProviderException
    var systems = _systemProvider.GetData();
    return new SystemComponentData
    {
        System = systems.Count > 0 ? systems[0] : ComponentData.Empty,
        Processors = _processorProvider.GetData(),
        Memory = _memoryProvider.GetData(),
        Disks = _diskProvider.GetData()
    };
}

Rationale:

  • Factories are orchestration layers - they shouldn't hide provider failures
  • Calling code should decide how to handle provider exceptions
  • All factory methods document that they may throw ComponentDataProviderException

4. Client Code Exception Handling

Client code should handle exceptions at appropriate boundaries:

try
{
    var factory = new ComponentDataFactory();
    var data = factory.Create();
    // Use data...
}
catch (ComponentDataProviderException ex)
{
    // Log provider details
    _logger.LogError(ex, 
        "Provider {ProviderName} failed querying {WmiClassName}", 
        ex.ProviderName, 
        ex.WmiClassName);
    
    // Handle gracefully - maybe use cached data or retry
}

5. Serialization Exceptions

Serialization classes use standard .NET exceptions since they deal with input validation:

public string Serialize<T>(T value)
{
    if (value == null)
        throw new ArgumentNullException(nameof(value));
    
    return JsonSerializer.Serialize(value, _options);
}

public T? Deserialize<T>(string data)
{
    if (string.IsNullOrWhiteSpace(data))
        throw new ArgumentException("Data cannot be null or whitespace.", nameof(data));
    
    return JsonSerializer.Deserialize<T>(data, _options);
}

Exception Documentation

All public methods that can throw exceptions should document them in XML comments:

/// <summary>
/// Retrieves the component data from hardware components.
/// </summary>
/// <returns>A collection of component data instances.</returns>
/// <exception cref="ComponentDataProviderException">
/// Thrown when the provider fails to retrieve data from the underlying data source.
/// </exception>
IReadOnlyList<ComponentData> GetData();

Testing Exception Scenarios

All exception paths should be tested:

[TestMethod]
public void ComponentDataFactory_GetProcessors_PropagatesProviderException()
{
    // Arrange
    var throwingProvider = new ThrowingComponentDataProvider();
    var factory = new ComponentDataFactory(throwingProvider, null, null, null);

    // Act & Assert
    var exception = Assert.ThrowsExactly<ComponentDataProviderException>(
        () => factory.GetProcessors());
    
    Assert.AreEqual("TestProvider", exception.ProviderName);
}

Decision Tree: Which Exception to Use?

Is it a programming error (null argument, invalid state)?
├─ YES → Use standard .NET exception
│  ├─ Null argument? → ArgumentNullException
│  ├─ Invalid argument value? → ArgumentException  
│  └─ Invalid operation for current state? → InvalidOperationException
│
└─ NO → Is it a domain/business logic error?
   └─ YES → Use IronLedger custom exception
      ├─ Provider failure (WMI/CIM)? → ComponentDataProviderException
      ├─ Component data operation? → ComponentDataException
      └─ Other IronLedger error? → IronLedgerException

Best Practices Summary

  1. ✅ DO use ArgumentNullException for null parameters
  2. ✅ DO use ArgumentException for invalid parameter values
  3. ✅ DO use ComponentDataProviderException for provider failures
  4. ✅ DO include the original exception as InnerException when wrapping
  5. ✅ DO populate ProviderName in provider exceptions
  6. ✅ DO document all exceptions in XML comments
  7. ✅ DO write tests for exception scenarios
  8. ❌ DON'T catch exceptions you can't handle meaningfully
  9. ❌ DON'T catch and re-throw without adding value
  10. ❌ DON'T hide failures in orchestration layers (factories)

See Also