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 trycatch 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?
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:
- After encountering an error, which line of code will it execute next?
- 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?
- 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:
- Device has disconnected
- 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.
I don’t see that using error codes is going to gain you anything over exceptions. It’s just as easy to miss checking an error code, and then what?
Also, consider the following example:
public void CallProvider2() {
var provider = new Provider2();
Result result;
string serialNumber = provider.GetSerialNumber(out result);
if (result.ResultCode != 0) {
//error handling
switch(result.ResultCode)
{
case 123:
break;
//more cases
default:
//what now?
break;
}
}
//success flow
}
The above is how you might handle errors, and the “default” case would catch unchecked error codes and would be the equivalent of an unhandled exception. It would be worthwhile to ask what should be done if that case is ever reached. You can’t continue, but you’ve got to safely exit that method or what? Are you just going to return the Result object all the way up the stack and check if ResultCode !=0 everywhere? Might as well stick to exceptions.
Other benefits of exceptions to consider:
1) You get a stack trace automatically, which can be enormously useful in debugging.
2) If you need to handle an error further up the call stack, exceptions propagate upwards automatically. You can also handle and rethrow them. In the ResultCode examples you’d have to write additional code to pass them around if needed.
3) .NET runtime projects have some variation of an UnhandledException event you can subscribe to and do some centralized error logging. Even if your app goes down, you can still get some information about why. The plumbing to do that is already built in, you just have to attach to it.
4) An app crash from an unhandled exception, while undesirable, is USEFUL. No one can mistake that something went wrong.
1
Very debatable question, that depends on many conditions of your project, workflow, whether your code will be used by other developers, etc.
Yes, the common .Net practice is to throw exceptions instead of returning an error code. Very good comment about it here.
Error codes are old skool, you needed them back in the bad old days of COM and C programming, environments that didn’t support exceptions. Nowadays, you use exceptions to notify client code or the user about problems. Exceptions in .NET are self-descriptive, they have a type, a message and diagnostics.
Thought, you need to have in mind other aspect – if the code of the error itself is also self-descriptive (like http codes, 404, 503, etc), then you can return the code. And the .Net framework actually returns such code with HttpWebResponse.StatusCode (though still wrapped with enum). But do it if you know for sure (he-he) that these codes won’t change in future.
In other cases, what sense to user of your code to know that it was -lets say – error #23452, that was occurred. Besides, it can be changed in future, that causes the user to override his code because of number changed.
1
When designing an API for operations which may fail because of foreseeable reasons, but at unforeseeable times, one should generally allow a means by which callers can indicate whether they are expecting to cope with the semi-foreseeable failures. If the only methods which are available return with error codes rather than exceptions, then every call site will be required to test the return value, even if the only thing it can do with an error code is throw an exception. If the only methods which are available throw exceptions for all failures, even ones a caller might be expecting, then every caller that would be prepared to handle a failure will have to use a try/catch for that purpose.
A common means for exposing methods whose callers may or may not expect failures is to have pairs of methods that fnorble named, e.g., TryFnorble
and Fnorble
. Suppose Thing1a
and Thing1b
have both styles of method, while Thing2a
only has the first and Thing2b
only has the second. Code which is prepared to handle a problem with the first thing but not the second could be written as:
Thing1a.Fnorble();
if (Thing1b.Fnorble())
Thing1bIsReady = true;
else
Thing1bIsReady = false;
By contrast, if the code was using Thing2a
/Thing2b
, it would need to be written as:
if (!Thing2a.Fnorble())
throw new FnorbleFailureException(...);
try
{
Thing2b.Fnorble();
Thing2bIsReady = true;
}
catch (Ex as FnorbleFailureException)
{
Thing2bIsReady = False;
}
Much less nice.
[Note: I use an if/else
and explcitly set/clear the Thing1bIsReady
flag, rather than simply assigning the return value directly to the flag, because even though the return type and flag are both bool
, I would regard them as having semantically different meanings. I would consider the fact that having the method return true
once would cause the flag to be set immediately, and having it return false
once would cause the flag to be cleared immediately as an implementation detail.