Using ::FormatMessage to report errors in MFC

Home
Back To Tips Page

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 use 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 hard 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))
      { /* Failed */
       DWORD err = ::GetLastError();                 // [1]
       CString fmt;                                  // [2]
       fmt.LoadString(IDS_FILE_OPEN_ERROR);          // [3]
       // File open error\n
       // %s\n
       // %s
       CString msg;                                  // [4]
       msg.Format(fmt, filename, ErrorString(err));  // [5]
       AfxMessageBox(msg, MB_ICONERROR | MB_OK);
       return FALSE;
      } /* Failed */
    ... read file here
    f.Close();
    return TRUE;
   } // CMyClass::ReadFile

This takes only five lines of code to do the job right.  Therefore, there is never a justification for the completely incompetent example 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 in 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"

/****************************************************************************
*                                 ErrorString
* Inputs:
*       DWORD err: Error code
* Result: CString
*       String message
****************************************************************************/

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)
	{ /* failed */
	 // Unknown error code %08x (%d)
	 CString fmt;
	 CString t;
	 fmt.LoadString(IDS_UNKNOWN_ERROR);
	 t.Format(fmt, err, LOWORD(err));
	 Error = t;
	} /* failed */
     else
	{ /* success */
	 LPTSTR p = _tcsrchr(s, _T('\r'));
	 if(p != NULL)
	    { /* lose CRLF */
	     *p = _T('\0');
	    } /* lose CRLF */
	 Error = s;
	 ::LocalFree(s);
	} /* success */
     return Error;
    } // ErrorString

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. 

ErrorString for WinSock

If you are doing network programs, you probably want to report network errors in a meaningful way.  This is the version of ErrorString I used in a network-based application.

ErrorString

This is a global function based on the above code.  However, this function additionally can produce text messages for network errors.

CString ErrorString(DWORD err)                                        // [1]
    {
     DWORD flags = 0;                                                 // [2]

     CString path;
     HMODULE lib = NULL;                                              // [3]


     LPTSTR p = path.GetBuffer(MAX_PATH);                             // [4]

     HRESULT hr = ::SHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, SHGFP_TYPE_CURRENT, p); // [5]

     if(SUCCEEDED(hr))                                                // [6]

        { /* succeeded */
         path.ReleaseBuffer();                                        // [7]

         if(path.Right(1) != _T("\\"))
            path += _T("\\");                                         // [8]

         path += _T("WSOCK32.DLL");                                   // [9]
  
         lib = ::LoadLibrary(path);                                   // [10]
         if(lib != NULL)                                              // [11]
            flags |= FORMAT_MESSAGE_FROM_HMODULE;                     // [12]
        } /* succeeded */
     else
        { /* failed */
         path.ReleaseBuffer();                                        // [13]
        } /* failed */     

     LPTSTR msg;                                                      // [14]

     if(::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |              // [15]
                        FORMAT_MESSAGE_FROM_SYSTEM |                  // [16]
                        flags,                                        // [17]
                        (LPCVOID)lib,                                 // [18]
                        err,                                          // [19]
                        0, // language ID
                        (LPTSTR)&msg,                                 // [20]
                        0, // size ignored
                        NULL) // arglist
                              == 0)
        { /* not found */
         return DefaultError(err);                                    // [21]
        } /* not found */

     LPTSTR p = _tcsrchr(msg, _T('\r'));                              // [22]
     if(p != NULL)
        *p = _T('\0');                                                // [23]

     CString s = msg;                                                 // [24]

     if(lib != NULL)
        ::FreeLibrary(lib);                                           // [25]
     LocalFree(msg);                                                  // [26]

     return s;
    } // ErrorString
[1] This function takes a DWORD error code and returns a CString. Radical OO types do not approve of global functions but want methods of singleton classes, but I think that is largely overkill.
[2] The FormatMessage call takes a set of flags, one of which will be optional.  This variable will hold that optional flag, and if the flag is not valid, the variable is initialized to 0.
[3] We are going to load a DLL which has the WinSock MESSAGETABLE and this HANDLE will hold the handle to the library.  It is initialized to NULL.
[4] We are going to call a system API which requires a character buffer of size MAX_PATH.  This creates the temporary buffer in a CString.
[5] The ::SHGetFolderPath API replaces the now-obsolete GetSystemDirectory API.  On a standard installation, it is likely that this will return the string "c:\Windows\System32".
[6] The ::SHGetFolderPath API returns a type HRESULT, and the way this is tested for success is with the SUCCEEDED macro.
[7] If the call succeeds, we must call ReleaseBuffer before using the CString.
[8] We want to make sure the path ends with "\", so if it doesn't, one is added.
[9] The DLL that holds the MESSAGETABLE is called "WSOCK32.DLL".  Now what seems odd about this is that for a 64-bit app, you might expect that you should be trying to load "WSOCK64.DLL", but it doesn't work that way.  The names, no matter how tasteless they were when they are chosen, seem to remain the same.  I verified this by using the dumpbin utility on WSOCK32.DLL, and got the following:
   Microsoft (R) COFF/PE Dumper Version 8.00.50727.42
   Copyright (C) Microsoft Corporation.  All rights reserved.
   Dump of file c:\windows\system32\wsock32.dll
   PE signature found  
   File Type: DLL  
   FILE HEADER VALUES
               8664 machine (x64)  
                  5 number of sections
           42438B57 time date stamp Thu Mar 24 23:53:59 2005
                  0 file pointer to symbol table
                  0 number of symbols
                 F0 size of optional header
               2022 characteristics
                       Executable
                       Application can handle large (>2GB) addresses
                       DLL
[10] The ::LoadLibrary API will load the DLL.  The handle for this DLL will be used in the FormatMessage call.
[11] The handle will be NULL if there was any failure.
[12] If the ::LoadLibrary succeeds, we can use the module handle, so the FORMAT_MESSAGE_FROM_HMODULE flag will be added to the set of flags used by FormatMessage.
[13] If the ::SHGetFolderPath fails, it is still necessary to do a ReleaseBuffer because otherwise the CString is in an undefined state and will probably cause some serious malfunction when its destructor is called.
[14] This will be a pointer to the buffer which will be allocated by the FormatMessage call.
[15] The FormatMessage call will retrieve the error string in the local language.  The FORMAT_MESSAGE_ALLOCATE_BUFFER tells the API that it should allocate a buffer to hold the message.
[16] The FORMAT_MESSAGE_FROM_SYSTEM flag indicates that the error code should be interpreted according to the system error table.
[17] If there was any failure trying to load the DLL, the flags value is 0 so only the system message table will be searched.  However, if the flags has been set to FORMAT_MESSAGE_FROM_HMODULE, then the specified module will be searched first; if the error code is not found in that module, then the system error table is searched.
[18] The meaning of this parameter varies with the flags.  The parameter is officially specified as an LPCVOID, and its interpretation when the FORMAT_MESSAGE_FROM_HMODULE flag is specified, this is interpreted as being the handle to a DLL that contains a MESSAGETABLE.  But to sneak this by the compiler, an explicit (LPCVOID) cast must be written.
[19] This is the 32-bit error code that will be checked against the error table(s).
[20] This parameter is officially specified as an LPTSTR pointer to a buffer into which the characters of the message will be written.  But if the FORMAT_MESSAGE_ALLOCATE_BUFFER flag is specified, it is actually a pointer to a pointer to a character buffer.  The API call will allocate the buffer, and store the pointer to this buffer in the location pointed to by this parameter.  So it must, when that flag is specified, by a pointer-to-an-LPTSTR, and to sneak this past the compiler, the explicit (LPTSTR) cast is required.
[21] The ::FormatMessage API call returns the count of the number of characters it returned, and if this is 0, the API call failed.  This means that it was unable to find the error code in any of the sources specified.  In this case, we revert to formatting a simple string.
[22] Due to what is poor design, every message gets a \r\n (CRLF) appended to the end of the message.  This is at best useless and at worst harmful.  This code checks to see if there is a terminal CRLF...
[23] ...and if so, overwrites the \r with a NUL character so the CRLF is truncated.
[24] The function is specified as returning a CString, so this variable represents that CString value that will be returned. The assignment causes a copy of the returned buffer to be made in the CString.
[25] If the DLL had been loaded, it must be freed. 
[26] The buffer that was allocated by the FormatMessage API must be freed.  The API that does this is LocalFree.
static CString DefaultError(DWORD err)
    {
     CString fmt;
     fmt.LoadString(IDS_DEFAULT_ERROR);
     CString s;
     s.Format(fmt, err, err);
     return s;
    } // DefaultError

 

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright © 2006, The Joseph M. Newcomer Co. All Rights Reserved
Last modified: May 14, 2011