C++ - Arguments for Exceptions Over Return Codes

C++ - Arguments for Exceptions over Return Codes

I think this article sums it up.

Arguments for Using Exceptions

  1. Exceptions separate error-handling code from the normal program flow and thus make the code more readable, robust and extensible.
  2. Throwing an exception is the only clean way to report an error from a constructor.
  3. Exceptions are hard to ignore, unlike error codes.
  4. Exceptions are easily propagated from deeply nested functions.
  5. Exceptions can be, and often are, user defined types that carry much more information than an error code.
  6. Exception objects are matched to the handlers by using the type system.

Arguments against Using Exceptions

  1. Exceptions break code structure by creating multiple invisible exit points that make code hard to read and inspect.
  2. Exceptions easily lead to resource leaks, especially in a language that has no built-in garbage collector and finally blocks.
  3. Learning to write exception safe code is hard.
  4. Exceptions are expensive and break the promise to pay only for what we use.
  5. Exceptions are hard to introduce to legacy code.
  6. Exceptions are easily abused for performing tasks that belong to normal program flow.

Conventions for exceptions or error codes

I normally prefer exceptions, because they have more contextual information and can convey (when properly used) the error to the programmer in a clearer fashion.

On the other hand, error codes are more lightweight than exceptions but are harder to maintain. Error checking can inadvertently be omitted. Error codes are harder to maintain because you have to keep a catalog with all error codes and then switch on the result to see what error was thrown. Error ranges can be of help here, because if the only thing we are interested in is if we are in the presence of an error or not, it is simpler to check (e.g., an HRESULT error code greater or equal to 0 is success and less than zero is failure). They can inadvertently be omitted because there is no programmatic forcing that the developer will check for error codes. On the other hand, you cannot ignore exceptions.

To summarize I prefer exceptions over error codes in almost all situations.

Which, and why, do you prefer Exceptions or Return codes?

For some languages (i.e. C++) Resources leak should not be a reason

C++ is based on RAII.

If you have code that could fail, return or throw (that is, most normal code), then you should have your pointer wrapped inside a smart pointer (assuming you have a very good reason to not have your object created on stack).

Return codes are more verbose

They are verbose, and tend to develop into something like:

if(doSomething())
{
if(doSomethingElse())
{
if(doSomethingElseAgain())
{
// etc.
}
else
{
// react to failure of doSomethingElseAgain
}
}
else
{
// react to failure of doSomethingElse
}
}
else
{
// react to failure of doSomething
}

In the end, you code is a collection of idented instructions (I saw this kind of code in production code).

This code could well be translated into:

try
{
doSomething() ;
doSomethingElse() ;
doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
// react to failure of doSomething
}
catch(const SomethingElseException & e)
{
// react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
// react to failure of doSomethingElseAgain
}

Which cleanly separate code and error processing, which can be a good thing.

Return codes are more brittle

If not some obscure warning from one compiler (see "phjr" 's comment), they can easily be ignored.

With the above examples, assume than someone forgets to handle its possible error (this happens...). The error is ignored when "returned", and will possibly explode later (i.e. a NULL pointer). The same problem won't happen with exception.

The error won't be ignored. Sometimes, you want it to not explode, though... So you must chose carefully.

Return Codes must sometimes be translated

Let's say we have the following functions:

  • doSomething, which can return an int called NOT_FOUND_ERROR
  • doSomethingElse, which can return a bool "false" (for failed)
  • doSomethingElseAgain, which can return an Error object (with both the __LINE__, __FILE__ and half the stack variables.
  • doTryToDoSomethingWithAllThisMess which, well... Use the above functions, and return an error code of type...

What is the type of the return of doTryToDoSomethingWithAllThisMess if one of its called functions fail ?

Return Codes are not a universal solution

Operators cannot return an error code. C++ constructors can't, too.

Return Codes means you can't chain expressions

The corollary of the above point. What if I want to write:

CMyType o = add(a, multiply(b, c)) ;

I can't, because the return value is already used (and sometimes, it can't be changed). So the return value becomes the first parameter, sent as a reference... Or not.

Exception are typed

You can send different classes for each kind of exception. Ressources exceptions (i.e. out of memory) should be light, but anything else could be as heavy as necessary (I like the Java Exception giving me the whole stack).

Each catch can then be specialized.

Don't ever use catch(...) without re-throwing

Usually, you should not hide an error. If you do not re-throw, at the very least, log the error in a file, open a messagebox, whatever...

Exception are... NUKE

The problem with exception is that overusing them will produce code full of try/catches. But the problem is elsewhere: Who try/catch his/her code using STL container? Still, those containers can send an exception.

Of course, in C++, don't ever let an exception exit a destructor.

Exception are... synchronous

Be sure to catch them before they bring out your thread on its knees, or propagate inside your Windows message loop.

The solution could be mixing them?

So I guess the solution is to throw when something should not happen. And when something can happen, then use a return code or a parameter to enable to user to react to it.

So, the only question is "what is something that should not happen?"

It depends on the contract of your function. If the function accepts a pointer, but specifies the pointer must be non-NULL, then it is ok to throw an exception when the user sends a NULL pointer (the question being, in C++, when didn't the function author use references instead of pointers, but...)

Another solution would be to show the error

Sometimes, your problem is that you don't want errors. Using exceptions or error return codes are cool, but... You want to know about it.

In my job, we use a kind of "Assert". It will, depending on the values of a configuration file, no matter the debug/release compile options:

  • log the error
  • open a messagebox with a "Hey, you have a problem"
  • open a messagebox with a "Hey, you have a problem, do you want to debug"

In both development and testing, this enable the user to pinpoint the problem exactly when it is detected, and not after (when some code cares about the return value, or inside a catch).

It is easy to add to legacy code. For example:

void doSomething(CMyObject * p, int iRandomData)
{
// etc.
}

leads a kind of code similar to:

void doSomething(CMyObject * p, int iRandomData)
{
if(iRandomData < 32)
{
MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
return ;
}

if(p == NULL)
{
MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
throw std::some_exception() ;
}

if(! p.is Ok())
{
MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
}

// etc.
}

(I have similar macros that are active only on debug).

Note that on production, the configuration file does not exist, so the client never sees the result of this macro... But it is easy to activate it when needed.

Conclusion

When you code using return codes, you're preparing yourself for failure, and hope your fortress of tests is secure enough.

When you code using exception, you know that your code can fail, and usually put counterfire catch at chosen strategic position in your code. But usually, your code is more about "what it must do" then "what I fear will happen".

But when you code at all, you must use the best tool at your disposal, and sometimes, it is "Never hide an error, and show it as soon as possible". The macro I spoke above follow this philosophy.

Is it good practice to return an error to the caller instead of throwing the error right away in C++

There are different common practices w.r.t. error handling in C++ - as it is a multi-paradigmatic language. For example:

  • Return status/error codes rather than results.
  • Return results only, throw exceptions on error.
  • Return value-or-error objects, such as std::expected.

Each of these has pros and cons. The most important thing is to be consistent in your program, and to coordinate with whoever calls your functions* - so that you meet their needs.

For a detailed presentation of current options and a future potential alternative, see this talk by Brand & Nash at the annual C++ conference CppCon:

CppCon 2018: "What Could Possibly Go Wrong?: A Tale of Expectations and Exceptions"

Error handling in C code

I like the error as return-value way. If you're designing the api and you want to make use of your library as painless as possible think about these additions:

  • store all possible error-states in one typedef'ed enum and use it in your lib. Don't just return ints or even worse, mix ints or different enumerations with return-codes.

  • provide a function that converts errors into something human readable. Can be simple. Just error-enum in, const char* out.

  • I know this idea makes multithreaded use a bit difficult, but it would be nice if application programmer can set an global error-callback. That way they will be able to put a breakpoint into the callback during bug-hunt sessions.

Hope it helps.

Error Handling Paradigms: Mixing Exceptions and Error Codes

I personally prefer exceptions (and have many good reasons for that). But of course throwing exceptions from DLL code to user code (and vice versa) is a bad idea. So in your situation I would use exceptions within modules (both DLLs and executable) and use error_return-based APIs for DLL exports. When using DLL's error_return-based API I would use exception-based wrappers for those error_return-ing functions.

IMO returning error indicators in any way (by return value or through reference or via per-thread error codes) havily messes the code with endless error-checking branches. My way (and my understanding of C++ way) is returning only in a case of success.

Why are exceptions so popular in C#/.NET programming compared to error codes in C++/Win32?

The main issue with error codes is that one needs to check them. Always.

And we are only human and can forget.

This can mean we can get our programs into inconsistent state by simply forgetting to check an error code.

If we forget to handle an exception, though, our program will exit.

This is seen as preferable to it continuing to run in an inconsistent state.

Much of the reason that error codes are still prevalent in C/C++ code is historical - these languages didn't have exception handling, so needed error codes. And there are many libraries and code out there that conforms to this idiom, so programmers need to keep with it.

There are other reasons to use exception apart from the fact that they cannot be ignored - they carry quite a lot of context, as Marc observed - stack traces, messages and more, beyond the type of the exception.

Why use exception instead of returning error code

Some of this might repeat content, but here are simple hints as to use one or the other:

No proper sentinel value

Your function might not be able to use a sentinel value to signal an error because all possible values are used by the function as valid answers. This may happen in different situations, most notably integer numerical algorithms. 0 and -1 are often used as special values but some fairly common algorithms, including pow might not be able to use those (i.e. 0^1=0 and -1^3=-1). Therefore, choosing proper values becomes somewhat of an art, consistancy is an issue and users have to remember the error value for each and every special case.

Some APIs recognize this and (almost) never use a real return value but consistently rely on return-by-reference semantics and use the return value in all functions as a status code, being either some (standard) "success" value, or a function-specific error code. CUDA, for instance, has such a convention.

Propagate by default

Error codes are often stated to be "more efficient" (a comment in one of the other answers explains why this is not necessarily true). However, they suffer from 2 common problems.

  1. You have to manually propagate the error up the call stack. This is often omitted (especially in example code and books, which is very irritating) because it litters the code with tiresome and error-prone handling code.
  2. Erros are diluted at high levels because error codes between different APIs are often impossible to concialiate. Moreover, designing you own error codes that cover the union of all libraries' error codes is a herculian task.This is why, in many applications, you get an The operation failed. message instead of Ran out of disk space..

To address (1), exceptions are propagated by default. Intentionally ignoring an error becomes obvious in the code, instead of hidden. To address (2), exceptions use type system, preventing you from compiling a program that has conflicting error "values". Morever, using an exception class hierarchy you can represent "families" of related results. (Note: this is often misused, and people catch Exception instead of NoMoreDiskSpace and still display the generic The operation failed. message).

A matter of consistancy

Some people will recommend a mixture of both in their applications, but IMHO, this leads to a situation where both the systems are mis-used. In these hybrid conventions, exceptions are often not caught and error-codes not checked because of confusion. On one hand, because exceptions are used only for exceptional situations, it is assumed they will never occur, or simply cannot be handled. On the other hand, a failure returning an error code is assumed to be minor and is not handled at all. And of course it is left up to each programmer to decide whether an situation is exceptional or not, leading to lots of confusion between what error codes to check and what exceptions to catch.

Which ever system you choose, be sure to use it at its full strength, and be consistent about its use.

Exceptions vs return codes : do we lose something (while gaining something else)?

how about:

for(int i = 0; i < n; i++)
{
try
{
myCall();
}
catch(Exception e)
{
Log(String.Format("Problem with {0}", i));
}
}


Related Topics



Leave a reply



Submit