Introduction
This code actually appears in nearly all my projects. However, a number of readers have missed this, so I'm putting this code here (there's no download, you can copy-and-paste from this Web page).
This shows a way of interfacing to ::FormatMessage
from MFC and using it in an MFC-compatible fashion.
The Problem
One of the common problems in handling errors is delivering complete, informative information to the end user. All too often, the problem is reported by one of these truly informative message boxes:
It is almost as useful as the one that says:
These are truly egregious examples which are all too common in far too many programs. The problem with this is that the user has no idea what to do in response to this error. Therefore, the user has to call tech support. What does this poor victim tell tech support? The contents of the dialog? You had better hope your tech support people don't know where your cubicle is, or you might meet with an unfortunate "accident" some day.
There is absolutely no excuse for such pitiful examples of error messages. None whatsoever.
The Solution
Now, this is an informative message:
It says there is a file open error (if I had more than one kind of file, even that line would be more precise), it says what file had the problem, and it says why. The user looks at this, realizes that the file is open in some other program, and closes that program. Or checks that the protections are properly set on the file. But does not feel compelled to call tech support.
How do you get that nice result? The answer is a remarkably few number of lines of code, which is why there is never an excuse for not producing a decent error message.
For example, to produce this message, I did:
BOOL CMyClass::ReadFile(const CString & filename)
{
CFile f;
if(!f.Open(filename, CFile::modeRead))
{
DWORD err = ::GetLastError();
CString fmt;
fmt.LoadString(IDS_FILE_OPEN_ERROR);
CString msg;
msg.Format(fmt, filename, ErrorString(err));
AfxMessageBox(msg, MB_ICONERROR | MB_OK);
return FALSE;
}
... read file here
f.Close();
return TRUE;
}
This takes only five lines of code to do the job right. Therefore, there is never a justification for the completely incompetent message given as the first example.
Note one important point here. The very first line that is executed after the API fails is ::GetLastError
. Do not put an ASSERT
before it; do not do anything else; capture that error immediately.
To accomplish this, the ErrorString
function must be written. However, given how trivial this function is to write, and that it only needs to be written once for your entire lifetime, there is little reason not to use it everywhere. To allow for localization, the string "Unknown error code %08x (%d)" is stored in the STRINGTABLE
as the string IDS_UNKNOWN_ERROR
.
I have never understood this phenomenon, but the first operating system that had an integrated error handling system, IBM's TSS/360, in 1968, would add a CRLF at the end of every error message string. This makes no sense. If I want a CRLF, I can add it. Nonetheless, Microsoft has continued this insanity nearly 20 years later. Each message ends with a CRLF. Why? There is no sensible explanation. So, I have to remove the terminal CRLF.
In addition, if you plan to use a message in some context that cannot understand newline sequences, such as a ListBox, you may wish to additionally do a CString::Replace(_T("\r\n"), _T(" "))
to get rid of gratuitous internal CRLF sequences.
ErrorString.h:
CString ErrorString(DWORD err);
ErrorString.cpp:
#include "stdafx.h"
#include "resource.h"
#include "ErrorString.h"
CString ErrorString(DWORD err)
{
CString Error;
LPTSTR s;
if(::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
err,
0,
(LPTSTR)&s,
0,
NULL) == 0)
{
CString fmt;
CString t;
fmt.LoadString(IDS_UNKNOWN_ERROR);
t.Format(fmt, err, LOWORD(err));
Error = t;
}
else
{
LPTSTR p = _tcsrchr(s, _T('\r'));
if(p != NULL)
{
*p = _T('\0');
}
Error = s;
::LocalFree(s);
}
return Error;
}
A Philosophy of MessageBox/AfxMessageBox Calls
There is no place, in any program I write, in which there are two places that issue the same MessageBox/AfxMessageBox
text. Just by looking at the text of the message, I can immediately tell what line of my program issued it. This is important. This makes it possible to determine the exact line of my program that issued the message.
Sometimes, if I have a generic error handler, I will pass in an extra CString
parameter which appears in the MessageBox/AfxMessageBox
so that the caller of the handler routine can be identified. There is no alternative to making sure that every user-visible error condition has a 1:1 correspondence with the site in the program that issues it.