Error Management

Started by
14 comments, last by kunos 11 years, 8 months ago
(c++)

Making code robust in the face of errors such as missing textures, files and models.

For textures, if I cannot find a texture I simply return a debug texture.
This code will always return a valid texture, preventing any possibility of crashes since it doesn't return NULL:

ITexture* texture = MaterialAPI()->GetTexture( "chicken" );


However, how can I check to see if an error has occurred in the process of loading a texture?
Something like this might work:

ITexture* texture;
bool succeeded = MaterialAPI()->GetTexture( "chicken", texture );

But how would you do it?

Similarly, some of my code just returns NULL on failure, such as when a file isn't found.

IStreamFile* file = FileAPI()->GetStreamFile( "config.txt" );

Here a failure to check for NULL will result in a crash. Maybe I can return a 'debug' empty file to prevent this in case of programmer error?

It seems to me returning NULL when you don't have to is asking for crashes as soon as something isn't just so, but I'm having some trouble deciding the best way to prevent this; At least as far as the engine interface APIs are concerned. Not only that, but returning NULL in some cases, and debug versions in others is simply not consistent.

Bonus Question:
Also, I surround my entry point with a 'try' statement, and during a fatal error I 'throw' with a specific type. It also allows me to catch any other potentially uncaught 'throw' statements and shutdown reasonably gracefully, but how would you guys handle this type of thing?

Thanks as usual!
Advertisement

However, how can I check to see if an error has occurred in the process of loading a texture?


You don't. The system returns a debug texture. If it can't load *that* texture, then it's time to die. Personally I favor returning null and letting the inevitable uncaught exception to take down the app. No need to write and test code that you've done everything you reasonably can to prevent and cannot really do anything about anyways.
Crash early.

It is much better to die immediately with some kind of fatal error than to try and limp along and risk potentially causing a lot of damaging side effects.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

"My policy for handling unexpected errors is simple: Crash the engine as soon as possible with an informative error message."
For more explanations behind this quote, see the source(s):
http://www.altdevblogaday.com/2012/01/22/sensible-error-handling-part-1/
http://www.altdevblogaday.com/2012/02/05/sensible-error-handling-part-2/
http://www.altdevblogaday.com/2012/02/20/sensible-error-handling-part-3/
Interesting philosophy and it makes sense.

I was trying too hard to prevent it from ever crashing I suppose and should take a more moderate approach.
Yeah the others had answered very well.
What I want to say is, never try to patch for your mistake (or to say, bugs).

I had worked on a software, when there were any null pointer crash, the original developers said "hey, let's check when it's null we just don't do that function".
What's that? They were hiding the problems rather than solving them. The result is, the software became harder and harder to maintain and we had to rewrite quite a bit of parts. If we continued that kind of "hiding problem", the software would have died before our expectation.

Now when I find a bug, I would think why it happens, rather than try to get a workaround for it.

Just my personal experience.

https://www.kbasm.com -- My personal website

https://github.com/wqking/eventpp  eventpp -- C++ library for event dispatcher and callback list

https://github.com/cpgf/cpgf  cpgf library -- free C++ open source library for reflection, serialization, script binding, callbacks, and meta data for OpenGL Box2D, SFML and Irrlicht.

You definitely want to catch and handle your errors in some way. Allowing the application to just work with a NULL pointer and crash when it tries to dereference it is going to tell you that you have a bug, but it's not going to help you find it. The same is true of letting exceptions bubble up and crash the application.

This is what I like to do. Every function that has any potential at all to cause an exception, I will enclose it in a try-catch block, add some context information to the exception for function/file/line number, then rethrow the exception so the next level function in the call stack can handle it and it's information. Then the main function, or the main thread function, will handle the exception by throwing a dialog box, writing to a log file, or both. This way you can get a lot of information about where and how the error is occurring, even in a release build.

Writing those try/catch blocks can be pretty tedious, so I use some macros to make life easier. i.e.:


#define TRYCATCH_OPEN \
try {

#define TRYCATCH_CLOSE \
} \
catch( Exception& e ) \
{ \
std::ostringstream dump; \
dump << e.message() << " " << __FUNCTION__ << "[" << __FILE__ << "(" << __LINE__ << ")]" << std::endl; \
if (e.nested() == nullptr) \
throw Exception( dump.str(), e); \
else \
throw Exception( dump.str(), *e.nested() ); \
} \
catch( std::exception& e ) \
{ \
std::ostringstream dump; \
dump << e.what() << " " << __FUNCTION__ << "[" << __FILE__ << "(" << __LINE__ << ")]" << std::endl; \
throw Exception( dump.str() ); \
}


Then when I throw my exceptions, I also use a macro so I can pin down exactly where the exception is being thrown. i.e.:


#define jp_throw(ExceptionType, ...) \
throw ExceptionType( what_(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__ ) );




Then every function/method has something like this:


void Foo::Bar()
{
TRYCATCH_OPEN

Some logic...
if (somecondition != expected)
jp_throw(SomeException, "somecondition is: ", somecondition, " expected: ", expected );

TRYCATCH_CLOSE
}



Still a little bit tedious, but much easier and cleaner than writing out all of those try/catch blocks, and it pays off.

I use custom exception classes that can contain nested exceptions, so I can preserve the original exception object while bubbling it up the chain.

This works out very well for me, and makes it much easier to track down bugs in beta and production builds without having to include debug information in the executable. The information isn't as precise as a true call stack, but it's much better than nothing.

As far as things like not being able to find a texture, I do it the way you do it, by returning a default debug texture. The only thing I do different is that I have a command-line option that can indicate that I want to throw an assertion first, so that I have the option of having the application complain loudly and definitively if there are missing resources, but also have the option of gracefully handling the situation so that development doesn't grind to a halt just because somebody broke the build with a missing resource.

it is going to tell you that you have a bug, but it's not going to help you find it.


I should've maybe clarified that I use a vaguely modern programming language that includes stack traces with its exceptions.

I should've maybe clarified that I use a vaguely modern programming language that includes stack traces with its exceptions.


A stack trace from a NULL pointer exception will only tell you where it is dereferenced, not where it originated.

And by "vaguely modern programming language" I guess you either A) are not talking about C++, which the question is about, B) think that errors are nice and friendly and only happen in debug mode, or C) bloat your release code with debug symbols and disable compiler optimizations.
The thing about the unforgiving crash, is that it can hinder progress on large teams, you use function A developed by programmer 1 which processes data introduced by scriptwriter or game designer X who uploaded a last minute change who then went to his dentist appointment and forgot his cellphone... you just lost a whole afternoon in the task you were doing because of someone else's mistake and there is nothing to do about it.

That is, after it happened, you can do things to minimize the impact of this situations, which in a 10+ team, tend to happen often, even in AAA development.
What you can do is having builds with different error tolerance:
The development build would be most permissive, trying to keep on going as much as possible and returning placeholder data or content whenever the real thing cannot be delivered.
The debug build can be the one that provides the most data, and is the least forgiving of all, crashing as soon as an error is detected and performing extra sanity checks (conditioned by pre processor code not to be part of the release).
Then the release build would lean towards speed (if we are speaking of videogames this is crucial) and resort to try catch code with error reporting support.

This way, if an error is introduced you can still keep on working without wasting your time, perhaps your progress would not be in a deliverable state due to the broken data on debug and release, but at least you will be able to make progress.
Game making is godlike

LinkedIn profile: http://ar.linkedin.com/pub/andres-ricardo-chamarra/2a/28a/272


This topic is closed to new replies.

Advertisement