Status codes have long gone the way of the Dodo --- made extinct by exceptions, actually.
The problem with a status code is that it can be ignored. An exception cannot. Consider these two examples.
Status Codes as Return Values
First, the "merchant repository" implementing CRUD operations for merchants:
public enum MerchantStatusCode
{
OK = 0,
Created = 1,
Updated = 2,
Deleted = 3,
MerchantAlreadyExists = 4,
MerchantDoesNotExist = 5
}
public class MerchantRepository
{
public MerchantStatusCode Create(Merchant model)
{
if (MerchantExists(model))
return MerchantStatusCode.MerchantAlreadyExists;
// Save to DB
return MerchantStatusCode.Created;
}
}
Now, the C# code to create a merchant and ignore error handling:
var merchantRepository = new MerchantRepository();
// Create a merchant. I'm not really sure what could go wrong, and if
// something should go wrong, I'm not really sure if it's a problem.
merchantRepository.Create(merchant);
The MerchantRepository.Create
method returns a status code that our programmer promptly ignores. By looking at these lines of code, there is no indication that you should do any error handling before subsequent lines of code are executed.
Status Codes as Output Parameters
Let's also say an output parameter is used:
public class MerchantRepository
{
public void Create(Merchant model, out MerchantStatusCode statusCode)
{
if (MerchantExists(model))
{
statusCode = MerchantStatusCode.MerchantAlreadyExists;
return;
}
// Save to DB
statusCode = MerchantStatusCode.Created;
}
}
And the code to ignore the status code:
var merchantRepository = new MerchantRepository();
MerchantStatusCode code;
// Create a merchant. Sure I could check the "status" but, meh. If
// something fails it must not be THAT important. After all, good
// programmers are lazy, right?
merchantRepository.Create(merchant, out code);
While this exhibits more of a code smell, an error code doesn't halt program execution.
Now, let's tweak this to throw an exception:
Using Exceptions
public class MerchantRepository
{
public void Create(Merchant model)
{
if (MerchantExists(model))
throw new RecordExistsException();
// Save to DB
}
}
public class RecordExistsException : Exception
{
...
}
And the C# code to use this in the same manner as the previous example where the programmer blissfully ignores problems:
var merchantRepository = new MerchantRepository();
try
{
merchantRepository.Create(merchant);
}
catch Exception
{
// I realize this method throws exceptions when it fails to create
// a merchant, but I am a complete idiot as a programmer, and
// failure as a human being, so I'm going to blindly swallow any
// possible errors, then move to another team or job and let you
// try tracking down this production problem for 6 months while
// the company bleeds customers and your boss screams at you so
// hard he spits his bagel and coffee all over your face.
//
// But, hey. At least I didn't write "TODO: Error handling"!
}
Both examples work the same when the code is executed and a duplicate merchant is created, but there is one glaring difference here: The empty try-catch
block. Empty try-catch blocks go beyond "code smells" to "steaming piles of rotting flesh that have been baking in the sun all afternoon." I get an ulcer every time I see this.
Now, if we tweak example #2 to be the exact same code as example #1:
var merchantRepository = new MerchantRepository();
// Even though I don't handle an exception, at least the next
// line of code won't execute if something goes terribly wrong.
// I'm not an idiot. I just HAVE to create a merchant here, and
// failure to do so is a hard stop.
merchantRepository.Create(merchant);
No subsequent lines of code will get executed, and the programmer does not have to guard against this condition unless they want to.
The lesson here: For the love of God do not blindly trap exceptions and do nothing, unless you document this behavior in comments and provide a really really really really really really really really really really really really really good reason for doing so.
Regarding your particular approach, there are a couple things that you should be doing instead:
Error Handling Best Practices Regarding Database Operations
- Prior to creating a merchant, check the database to ensure the merchant doesn't already exist. This is basic data validation, and should be handled in its own layer.
- When creating a merchant, if a merchant happens to exist, throw an exception which will force you, as a programmer, to handle this "exceptional situation" (hence the term Exception).
When a database operation is invoked, it needs to assume that all data is valid, and then throw an exception if anything goes wrong. Things should never be wrong in the first place, and the data access layer has no way of remedying this problem, so throwing an exception halts any further operation. Failing to create a merchant is a catastrophic, unresolvable issue requiring the immediate cessation of logic.
If attempting to create a duplicate merchant is something you can handle, then your Controller needs to trap this exception and handle it.
An interesting blog post about error codes and exceptions:
Error codes or Exceptions? Why is Reliable Software so Hard?
There can be a case for either, but I generally lean towards exceptions because they halt application execution, which is what you want most times.
out
is almost always a sign your function is doing too much. Return codes are as well. You should throw an exception if you have an issue with creating a new merchant. If the merchant already exists, then you should either be checking that the merchant exists based on predefined behaviour prior (merchant with the same name) - or you should allow the function to fail silently if one already exists. An exception is not used for flow control, it is used for an exceptional circumstance - a merchant already being created is an exceptional circumstance that the programmer should be aware of. \$\endgroup\$