You must use both. The point is to decide when to use each .
There are several scenarios where exceptions are an obvious choice :
In some situations, you cannot do anything with the error code , and you just need to process it at the top level in the call stack , as a rule, register an error, display something to the user or close the program. In these cases, error codes will require that you reset the error codes manually to a level that is obviously much easier to do with exceptions. The fact is that this is for unforeseen and unreliable situations.
However, about situation 1 (where something unexpected and inexplicable happens, you just don’t want to register it), exceptions can be useful because you can add contextual information . For example, if I get a SqlException in my junior data helpers, I want to catch this error at a low level (where I know the SQL command that caused the error) so that I can capture this information and re-challenge with additional information. Pay attention to the magic word here: rethrow, not swallow . The first rule of exception handling: does not swallow exceptions . Also, please note that my internal catch does not need to be registered, because the external catch will have all the stack trace and can register it.
In some situations, you have a sequence of commands, and if any of them fails , you must clear / delete resources (*), regardless of whether it is an unrecoverable situation (which should be selected) or a restored situation (in this case you can handle locally or in caller code, but you don't need exceptions). Obviously, it’s much easier to put all these commands in one attempt, instead of checking the error codes after each method and cleaning / deleting in the finally block. Note that if you want the error to bubble (which you probably want), you don’t even need to catch it - you just use it permanently to clean / delete - you must use catch / retrow if you want to add contextual information (see bullet 2).
One example is the sequence of SQL statements inside a transaction block. Again, this is also an “uncontrollable” situation, even if you decide to catch it at an early stage (treat it locally and not bubble up), it is still a “fatal” situation, of which the best result is to interrupt everything or, at least interrupt most of the process.
(*) This is similar to the on error goto we used in the old Visual Basic
In constructors, you can only create exceptions.
Having said that in all other situations where you are returning some information on which the caller MAY / SHOULD take some action , using return codes is probably the best alternative. This includes all the expected "errors" because they probably should be handled by the immediate caller and it is unlikely that too many levels will bubble up on the stack.
Of course, you can always consider expected errors as exceptions, and then immediately go one level higher, and you can also cover every line of code in an attempt to catch and take action for every possible error. IMO, this is a poor design, not only because it is much more detailed, but specifically because the possible exceptions that can be selected are not obvious without reading the source code - and exceptions can be selected from any deep method, creating invisible gotos . They break the structure of the code by creating several invisible exit points that make the code difficult to read and verify. In other words, you should never use exceptions as flow control , because it will be difficult for others to understand and maintain. It may even be difficult to understand all the possible streams of code for testing.
Again: for proper cleaning / deletion, you can use try-finally without catching anything .
The most popular criticism regarding return codes is that "someone can ignore error codes, but in the same way, someone can also swallow exceptions. Easy exception handling is easy in both methods. But writing a good code-based program errors are still much easier than writing an exception-based program, and if for some reason you decide to ignore all errors (the old on error resume next ), you can easily do this with return codes, and you cannot do it without much number of try- templates catch
The second most popular criticism about return codes is that “it's hard to bubble” - but this is because people don’t understand that exceptions are for unreacted situations, and error codes aren’t.
The solution between exceptions and error codes is the gray area. It’s even possible that you need to get the error code from some reuse of the business method, and then you decide to wrap this in an exception (perhaps by adding information) and let it bubble. But this is a design error, suggesting that ALL errors should be selected as exceptions.
Summarizing:
I like to use exceptions when I have an unforeseen situation in which there are not many, and usually we want to interrupt a large block of code or even the entire operation or program. This is similar to the old "on error goto".
I like to use return codes when I expected situations in which the caller code may / should take some action. This includes most business methods, APIs, validations, etc.
This distinction between exceptions and error codes is one of the principles of designing the GO language, which uses "panic" for unpredictable unforeseen situations, while regular expected situations are returned as errors.
However, on GO, it also allows multiple return values , which helps a lot in using return codes, since you can return an error and something else at the same time. In C # / Java, we can achieve this with parameters, Tuples or (my favorite) Generics, which, combined with enumerations, can provide explicit error codes to the caller:
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options) { .... return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area"); ... return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order); } var result = CreateOrder(options); if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
If I add a new possible return to my method, I can even check all the callers if they cover this new value in the switch statement, for example. You really cannot do this with exceptions. When you use return codes, you usually know all the possible errors in advance and check them. With exceptions, you usually don't know what might happen. Wrapping enums inside exceptions (instead of Generics) is an alternative (as long as it clears the type of exceptions that each method will throw), but IMO is still a bad design.