C# Error Handling – Exceptions vs Error Codes When Working with Devices

cerror handlingnetpatterns-and-practicesprogramming practices

Out team is at the cusp of a new project.
One of the components at the boundary of the system is the component which interacts with a printer through an external COM component (referenced as a usual dll).

The COM component returns integer codes if there were an error whilst command execution.
Lets consider a concrete code.

  public class OperationException:Exception {
    public int ErrorCode { get; private set; }
    public string ErrorDescription { get; private set; }

    public OperationException(int errorCode, string errorDescription) {
        ErrorCode = errorCode;
        ErrorDescription = errorDescription;
    }
}

//The exception throwing way
public class Provider1:IFrProvider {
    private readonly IDrvFR48 driver;

    public string GetSerialNumber() {
        //must read status before get SerialNumber
        int resultCode = driver.ReadEcrStatus();
        if (resultCode != 0) {
            throw new OperationException(resultCode, driver.ErrorDescription);
        }
        return driver.SerialNumber;
    }
}

//The way of out parameters returning.
public class Provider2 : IFrProvider
{
    private readonly IDrvFR48 driver;      

    public string GetSerialNumber(out Result result) {
        //must read status before get SerialNumber
        int resultCode = driver.ReadEcrStatus();           
        if (resultCode != 0) {
            result = new Result(resultCode, driver.ErrorDescription);
            return null;
        }
        result = new Result(0, null);
        return driver.SerialNumber;
    }
}

//The way of LastResult property setting.
public class Provider3 : IFrProvider
{
    private readonly IDrvFR48 driver;

    public Result LastResult {
        get {
            return new Result(driver.ErrorCode, driver.ErrorDescription);
        }
    }   

    public string GetSerialNumber() {
        //must read status before get SerialNumber
        if (driver.GetECRStatus() == 0)
            return driver.SerialNumber;
        return null;
    }
}

public class Result {
    public int ResultCode { get; private set; }
    public string Description { get; private set; }

    public Result(int resultCode, string description) {
        ResultCode = resultCode;
        Description = description;
    }
}

public class Caller {
    public void CallProvider1() {
        var provider = new Provider1();
        try {
            string serialNumber = provider.GetSerialNumber();
            //success flow
        }
        catch (OperationException e) {
            if (e.ErrorCode == 123) {
                //handling logic
            }
        }
    }

    public void CallProvider2() {
        var provider = new Provider2();

        Result result;
        string serialNumber = provider.GetSerialNumber(out result);
        if (result.ResultCode == 123) {
            //error handling   
        }
        //success flow
    }

    public void CallProvider3()
    {
        var provider = new Provider3();

        string serialNumber = provider.GetSerialNumber();
        if (provider.LastResult.ResultCode == 123) {                
            //handling               
        }
        //success flow                
    }
}

So we have three ways of error handling. The most specific thing in all this stuff is that each operation can fail, because it depends on a device.

Our team think of using out parameters, because of unhandled exceptions propagation fear. The other reason, connected with the fear, is that we will have to cover all calls by try\catch blocks to stop to be afraid, but such a code will be pretty ugly.

What pros and cons do you see and what could you advice?

Best Answer

The fear with exception can be eased by learning proper exception handling programming style (and also general error handling techniques).

Proper error handling (applicable to all mechanisms, and particular applicable to exception handling) requires careful reasoning as to the consequences and interpretation of each failed operation. Such reasoning entail:

  1. After encountering an error, which line of code will it execute next?
  2. After encountering an error, what does it say about the current state of the device?
    • Can the device resume normal operation?
    • Does the device need a reset?
    • Does the human user need to be notified?
  3. After encountering an error, is the program state still consistent?
    • Should the program do some cleanup to restore the program state's consistency?

This reasoning can be extended to any programming black-boxes such as a third-party library.


Code example.

To ensure that every successful call to QueryStatusStart is followed up with a call to QueryStatusFinish with other calls in between:

bool queryStatusStarted = false;
try
{
    // throws exception if fails
    device.QueryStatusStart(); 

    // this line is not executed unless the previous line succeeded.
    queryStatusStarted = true; 

    // might throw exception
    device.MakeMoreQueryStatusCalls(); 
}
finally 
{
    // Whether or note MakeMoreQueryStatusCalls() throws an exception,
    // we will reach here. Hence we can restore the device to a 
    // known state.

    if (queryStatusStarted)
    {     
        device.QueryStatusFinish();  
    } 
}

In general programming, status cleanup methods (such as the QueryStatusFinish method above) should not throw exception (because throwing an exception in a catch{} or finally{} block will indeed cause the uncaught exception problem you feared.

However, when dealing with a physical device, there is a need to interpret what a failed cleanup (restoration) means:

  1. Device has disconnected
  2. Device has malfunctioned and require human intervention (such as a power reset)

In either case, the program will not be able to continue its normal operation. The correct way to handle this situation varies from case to case.