What should an error message look like?
An error message should be meaningful to the user and to the developer. A user should be able to look at an error message, and understand broadly what the cause of the error is. A developer should be able to look at an error, and know at what line in his code this error occurred, and why it occurred. At the same time, commercial programs would not want to reveal too much about their code in the error message.
Samples of bad error messages would be:
This is how I think a good error message should look like:
- "File could not be played. Error!"
This error message helps neither the user nor the developer understand what the problem is.
- "Pointer m_pFilestream was NULL in file x:\projects\file_read.cpp, line 106. Behaviour unexpected. Deleting class object CStream, and returning!"
This error message may make sense to a developers, but would confuse 95% of all users. But even for a developer it might lose its utility, considering that as the project evolves, line numbers change, and code may be moved from file to file.
- "Access violation at address 0x44b32a. The program has performed an illegal operation and will be shut down."
This is one of the worst possible error messages. It doesn't tell the user anything, and doesn't tell the developer much either.
- Popping up multiple error messages when a chain of errors occur. For example, you click a button and get a message box : "Error, invalid filename". You click OK, and get a second message box : "Playback could not start, because file could not be opened." You click OK again, and get "Application error!"
- An error message should be divided into an advanced and basic mode. The basic mode tells the user that there is an error, and why the error occurred. The advanced mode provides enough information for the developer to fix the problem with. In basic mode, a user should see something similar to :
"The file you selected could not be played, because the format is probably not supported, or the selected file is corrupt. Please press the help button to view the list of supported input formats."
If the user selects the advanced mode, he sees information such as :
"Please contact technical support with the following information: HRESULT = E_POINTER, object m_pFilestream in function RenderFile(), class CStream*, was NULL. Error progression : Error triggered in RenderFile(), progressed to CStreamWrapper::StartFileRender(), ended at CMyApp::StartFileRender(), with error chain sealed at CMyApp::StartFileRender(). Release Build: 4997, OS=WinXP build 3342, MMDrivers=ReleaseVer 8.11, ParameterFlagDump=8772629"
You notice that function names and class names are included, and line numbers and filenames are left out. Also included is some general information about the OS, as well as the version of the required drivers. The parameter dump can specify a bit mask of all selected options. Also, and importantly, one sees the progression of the error. Just saying that the error occurred in RenderFile might sometimes not be sufficient if this function is called from various points in the application.
For commercial software companies, it might not be wished for to include the names of classes and functions in the error message. In this case, these names can be represented with numeric values which can then replace the names when the application is compiled as a release version. When an error report is received, the numbers can then be replaced with the function names using a reference file.
Implementation in C++
When designing an error handling mechanism in object oriented languages, there is always the problem of creating too many dependencies. A basis tenet of OOP is code reuse, and if you have a CErrorInfo class which every function returns, it becomes difficult to take a class out of this project and adding it to another project without having to change a good amount of code. As a result, whatever error handling mechanism we use should still allow our classes to be used in every other project, and adapted to other types of error handling without problems.
The method I prefer is to add a FireError macro at global scope, which calls a central error handling routine in our main class. The FireError macro could be declared this way:
#define FireFatalError(errNumber, errDescription)
::FireErrorImpl(errNumber, errDescription, typeid(this).name(), __func__, __FILE__, __LINE__, ErrLevelFatal);
[See Appendix 1 for information about the __func__ macro using Visual C++ 6.]
A piece of code could look like this:
If (m_pFileStream == NULL)
FireFatalError(E_POINTER, "Object m_pFilestream in %location was NULL");
If this code was taken to a different project where popping up message boxes was the preferred error handling mechanism, all I would have to do is
#define FireFatalError(errNumber, errDescription) ::MessageBox(NULL, errDescription, NULL, NULL);
and my class is fully useable once again.
Note: __func__ returns the name of the function. __FILE__ returns the name of the file, and __LINE__ returns the line. When RTTI is enabled with C++ applications, typeid(this).name() returns the name of the class.
The OS information, as well as the build information is retrieved by the central error handling function.
What remains right now is the progression of errors. In the code above, E_POINTER, an error value is returned to the calling function. That function that receives this error value will also fire a fatal error, and its error will be added to the existing error being displayed as part of the error chain. The last function in the chain will seal the chain, indicating that it is not going to route the error anymore. One can either display the error box when the chain is sealed, or when the first error is fired.
C++ comes with it's own error handling mechanism, the throw and catch calls. However, these are not very often used. Though not difficult, the throw-catch methods are unnecessarily complex, and make it difficult for you to find out exactly where the error occurred. It is often a matter of personal preference, and I prefer not to use throw-catch, as do a large number of C++ developers. A discussion about the disadvantages is available here.
1.) The __func__ (or __FUNC__) macro is available on some C++ compilers, but not on all. For these lacking compilers however, there are usually various hacks that simulate it. There is one for Visual C++ 6 here.
2.) For COM programmers, IErrorInfo is a good way to make error messages available to clients. A tutorial is available here.
3.) Information about using throw-catch as an error handling mechanism is available here and here.