Exceptions, Asserts, or something else?

Started by
15 comments, last by Antheus 15 years, 10 months ago
I'm building a framework for an engine, which later I will build my game upon, and I'm wondering what I should use in terms of reporting internal failure. My current options are: Loggers, try/catch/throw, Asserts (Win32), or just return true/false and let the upper levels deal with it. What are the Adv/Dis of all those options? I'm more in favour of return true/false, but that is just me. Should I switch to try/catch and be diligent enough to put it all over my code?
------------Anything prior to 9am should be illegal.
Advertisement
First thing to keep in mind, there is no one-size-fits-all approach for errors. Different kinds of errors need different approaches.

Asserts are good for catching things that should never happen, and which would have disastrous consequences. Null pointer checks and array out-of-bounds accesses are examples of things good to wrap in asserts. The disadvantage of asserts is that, if one is triggered, it kills your app with no hope of recovery. But some errors are so bad that it's better to just die immediately.

Returning true/false for errors (or returning some kind of error code) is effectively pretty similar to throwing exceptions. A short comparison:

Disadvantages of returning an error code:
- it's sometimes a pain to check the result of every call, especially if there are several nested calls.

Disadvantages of exceptions:
- try/catch blocks are usually much more verbose (and harder to read, in my opinion) than just checking an error result
- more importantly, every single part of the code that could be interrupted by an exception needs to properly handle being interrupted. This means using things like auto_ptrs. It's pretty easy to accidentally write non-exception-safe code.

I think it comes down to a style choice. My preference is to just return errors.

Logging is also excellent. Logging is good to highlight things that are unusual but not necessarily errors.
I use at least two of those.

At first, I use exceptions to verify internal assumptions, so for example something like:

def fib(n):    assert n >= 0    if n == 0 or n == 1:        return 1    result = fib(n-1) + fib(n-2)    assert(result > 0)    return result


After your code works, you can make python optimize those assertions away.



On the other hand, you might have input from the outer world (that is, from outside of your framework). In order to communicate with those, I would use exceptions in order to tell them they did something wrong. Just returning special values is problematic, as such special values either doesn't easily exist (think of a function returning a boolean in a statically typed language. Tell me a magic error value.) or checking it becomes annoying. Imagine such PHP-code:

connection = mysql_connect(...);if(!connection) explode();selection = mysql_select_db(connection,...);if(!selection) explode();result = mysql_query(connection,...)if(!result) explode();...


This was pretty much the reason why exceptions were developed.


Logging should be done all the time, because more data makes debugging much easier. You just need to consider how much you log. Just puttiong something in a logfile every five minutes might be a bit too imprecise... but on the other hand, you probably would not want to recreate my suicidal loglevel that just logged *EVERYTHING*. Function calls, simple decisions, everything. I found the bug really darn fast with that debuglevel, but just running the program for a few seconds created several megabytes of logfile, heh.
Assuming C++ ...

Loggers
I personally find loggers to be rather useless.

Loggers report your program's state at particular points of execution. At what points should one log program state? At the beginning of each function? Why not just insert a logging statement between every line of your program? Which variables should you log -- why not all of them?

Loggers add large maintenance cost to your project while being an utterly impotent debugging tool.

If you must log something, just use the language's built in logging facilities: clog and cerr.

Exceptions
Exceptions are of prime importance in C++ because the are the only way to signal an error in a constructor. They also prevent memory leaks by properly destroying automatic lifetime variables during stack unwinding (TBH, inexperienced programmers can actually cause more memory leaks by not being mindful of exception safe programming techniques)

You use exceptions to signal "exceptional" errors. Something like incorrect user input is not the place for an exception. However inability to initialize a resource at runtime is a good place to throw an exception.

Unlike error codes, exceptions are self propagating and unignorable. This means code is being constantly verified during development, resulting in fewer errors in the long run.

Lastly, they are great debugging tools because exceptions can produce a stack trace when an error occurs.

Assertions
You should be aware that assertions have nothing to do with WIN32 in particular. You should be using assertions heavily. You simply assert things in your program what should always be true. These are used to prevent your program from continuing with bad state as a result of programmer error (as opposed to user error).

Return values
To be brief: error codes are the devil and in most cases the best return type for the job is bool.
To add to fpsgamer's comments on assertions...

Using the standard assertions in <cassert>, assertions are compiled away in release mode. This makes them useful only for checking logic during development*. I find they're good for checking pre and post conditions of functions. If there's a possibility that something can fail based on input, you need something more than an assert.


* You could of course roll your own assert to do whatever you want. YMMV.
Unlike fpsgamer, the logger was the single most useful tool I ever put into the engine. I don't know how I managed without it.

It's also useful for user-side error reports. If there's a problem, ask the player to email you the engine.log, and if it's ridden with "Warning: Material file 'crate03.mat' contains no data" you can put it down to a bad installation.

"The right, man, in the wrong, place, can make all the dif-fer-rence in the world..." - GMan, Half-Life 2

A blog of my SEGA Megadrive development adventures: http://www.bigevilcorporation.co.uk

I think a lot of this has been covered. Asserts are great for checking things that you don't expect to ever happen (e.g.: a programmatic bug) and that would be difficult to recover from. My caution with asserts is always that if it does happen on a non-debug build, you hit an area that you really never encountered or tested before and you may have no recovery code. I basically use asserts when adding recovery code would be too expensive. For example, doing something on a per pixel operation would be deadly to do a parameter check on every pixel. Ultimately, though, parameter validation is another area where asserts excel because they won't exist in "release" code.

Error codes are awful. Boolean values if it's truly a boolean condition that isn't necessarily an error. Never have a stringy error parameter to a function to try to return error data. If you're doing this, you want an exception. If it truly is an error, exceptions are king -- mainly because they don't ugly-fy an otherwise clean looking interface and they're harder to ignore. But, please please document the methods that will throw in some way, otherwise exceptions may be the most dangerous mechanism.

Finally, logging is great. I disagree with use of clog and cerr, unfortunately. They don't offer enough configuration options in my experience. One thing I've made use of is the dynamic ability to enable or disable logging of certain components of a codebase. The standard facilities don't allow for this -- it's all or nothing. Anyway, if you ever work with embedded, you will realize that logging is almost essential, since the remote debuggers are often nowhere near VC++ quality.
Wow, lest fpsgamer misguide someone:

Quote:Loggers add large maintenance cost to your project while being an utterly impotent debugging tool.
If you must log something, just use the language's built in logging facilities: clog and cerr.

How do you propose to remote-debug script errors (that don't result in a crash of the process) without a very detailed log?
clog and cerr are also far too slow to call frequently, as would be useful for a "last known activity" kind of log.

Quote:Unlike error codes, exceptions are self propagating and unignorable. This means code is being constantly verified during development, resulting in fewer errors in the long run.

I'll give you the "self propagating", but it is also possible to make error code objects unignorable. Yet another compelling alternative is to create a simple WARN_AND_RETURN macro that raises a warning and returns an error code when a failure condition is detected. This allows a much finer level of control - you can allow non-developer users to continue or exit or 'debug' (raising an exception such that a minidump is generated for you).

Quote:Lastly, they are great debugging tools because exceptions can produce a stack trace when an error occurs.

So can "if(condition) debug_dump_stack();"

Quote:To be brief: error codes are the devil and in most cases the best return type for the job is bool.

Bad advice! Assume a LoadFile function, where there are several error modes that clients must be able to distinguish (not found, no permission, sharing violation, out of memory, ..). Bool just tells you something went wrong - and that's in the best case, i.e. that other devs understood the meaning correctly (what if you pass return codes as a parameter to a function? Does it mean "ok and continue" or "error condition detected"?). To divine the actual cause, you'd then need a separate GetLastError, in which case you'd have been better off just returning that value outright.
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3
Quote:Original post by fpsgamer
Unlike error codes, exceptions are self propagating and unignorable. This means code is being constantly verified during development, resulting in fewer errors in the long run.


I don't know if "unignorable" is the right word. For both error codes and exceptions, I could ignore the possibility of errors, and end up writing bad code.

A short contrived example:
  FileReader *fileReader = new FileReader();  anyErrors = fileReader->readFile("somefile.dat"); // <-- This file doesn't exist  doSomethingWithData(fileReader->data);'  delete fileReader;


So, this code ignores the possibility that "somefile.dat" doesn't exist.

If the "readFile" method throws a FileNotFound exception, then my code has a memory leak. If "readFile" returns an error, then the code would keep executing, and "doSomethingWithData" will probably die. The first kind of error is surprising and very difficult to track down. The second kind of error is pretty easy to track down. That's why I don't like using exceptions in C++.

If we were talking about a garbage-collected language, then I'd be much more in favor of exceptions.
As others have said before, there is no one way of handling this that is ideal for all possible use cases. I use exceptions-hierarchies for many things, but in some cases I prefer to define an op result enum and then use that to return error codes instead, mostly in situations where an error might occur relatively often or throwing exceptions might be inefficient for performance reasons.

I also strongly disagree with the poster who claimed that logging is useless for debugging. We have a couple of projects where extensive use of detailed log messages have helped us track down some nasty bugs.

This topic is closed to new replies.

Advertisement