Click here to Skip to main content
15,888,301 members
Articles / Programming Languages / C++
Article

Using FormatMessage

Rate me:
Please Sign up or sign in to vote.
3.33/5 (7 votes)
4 Feb 20023 min read 172.7K   692   30   17
An example of one of the most effective uses of the the call to FormatMessage

Introduction

The FormatMessage API call is very powerful, and particularly useful for issuing all kinds of messages. This is not intended to be a full tutorial on using FormatMessage, but simply an example of one of the most effective uses of the the call. There is nothing more offensive than being presented with a message like the one below:

Image 1

This is incomprehensible. The file cannot be opened. Why? For that matter, what file? The name the user types in is only part of the filename context; the rest relies on the current directory, which may not be what the user thinks it is. Errors like this generate unnecessary support calls to tech support departments, and consume a lot of time of the technical support people trying to determine what has gone wrong. A much better example of an error message is the next one. It tells what the application-level error is, what file was being considered, and the description of the error code.

Image 2

In this case, I had created a read-only file junk.txt to induce the error on a Save command.

To make this work, I use the FormatMessage call. I then convert the value to a CString. If no error message is found, I handle the error by formatting the message with a particular string, which is stored in the STRINGTABLE

IDS_UNKNOWN_ERROR some valueUnknown error 0x%08x (%d)

You will have to add this STRINGTABLE entry to your application. The string number doesn't matter. At the call site, you can do something like

CStdioFile f;
if(!f.Open(dlg.GetPathName(), CFile::modeWrite | CFile::modeCreate))
   { /* failed */
DWORD err = ::GetLastError();
CString errmsg = ErrorString(err);
    CString fmt;
    fmt.LoadString(IDS_OPEN_FAILED);
CString s;
s.Format(fmt, dlg.GetPathName(), errmsg);
AfxMessageBox(s);
return;
   } /* failed */

The message formatting string is stored as a resource in the STRINGTABLE. This allows for internationalization.

IDS_OPEN_FAILED some valueFile Open failed\n%s\n%s

One of the peculiar features of FormatMessage, which makes absolutely no sense at all, is that it appends a newline sequence to the message. This is rarely, if ever, of value, and as  far as I can tell, having worked in three other major operating systems that supported a FormatMessage equivalent, this seems to arise from some fundamental misunderstanding of the problem. In every single one of these systems, dating back to 1968, I have had to eliminate the newline sequence from the error string before it was usable! So I delete this newline sequence from the result of FormatMessage so the message can be placed in a MessageBox, CEdit, CListBox, or other such place and make sense. (The peculiar attitude seems to be that the designer of the error message should control this, when in fact it should either not be appended at all, or be an option flag that defaults to "off" in the call to retrieve the message).

Just for your information, if you want to get the error text while debugging, place in the watch window of the debugger the request

@ERR,hr

Image 3This is a screen snapshot of the watch window taken after the CStdioFile::Open call failed. As you can see, the error message is nicely explained to you, the programmer. When the error is something that needs to be displayed to the user, you can use my ErrorString function to get a nice CString. Note that even if you think the message code is of no use to the user, display it anyway. For example, a message of the form is actually very useful. Here is a simulated display of an AfxMessageBox. Your tech support people will be greatly aided.

<FONT face=Arial size=2>Your Application Name Here</FONT>
Unable to complete operation. Internal error.
Unexpected error code from network support.

( An attempt was made to establish a session to a network server, but there are already too many sessions established to that server).

Perhaps you did not expect this error and don't really know what to do when it occurs, but your tech support people could waste hours trying to track down a problem that is not really in your program, but an artifact of an overloaded server.

Here is the code of the function in its entirety: The download consists of a compilable .cpp file and the accompanying .h file.

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 = _tcschr(s, _T('\r'));
     if(p != NULL)
        { /* lose CRLF */
         *p = _T('\0');
        } /* lose CRLF */
     Error = s;
     ::LocalFree(s);
    } /* success */
     return Error;
    } // ErrorString

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Retired
United States United States
PhD, Computer Science, Carnegie Mellon University, 1975
Certificate in Forensic Science and the Law, Duquesne University, 2008

Co-Author, [i]Win32 Programming[/i]

Comments and Discussions

 
GeneralMy vote of 4 Pin
Arun_angel17-Dec-12 3:10
Arun_angel17-Dec-12 3:10 
GeneralPerformance Question Pin
Ross White17-Aug-09 5:52
Ross White17-Aug-09 5:52 
GeneralRe: Performance Question Pin
Joseph M. Newcomer17-Aug-09 6:14
Joseph M. Newcomer17-Aug-09 6:14 
Questionupdate, minor complaint, and request Pin
bkelly1324-Aug-08 15:27
bkelly1324-Aug-08 15:27 
AnswerRe: update, minor complaint, and request Pin
Joseph M. Newcomer24-Aug-08 18:41
Joseph M. Newcomer24-Aug-08 18:41 
The parameter should have been written as
(LPTSTR)&lpBuffer

because that is how you have to write it to get it to compile. Note that is how I wrote my code, so you have no reason to expect that doing it some other way and using an illegal type should compile, or, as you have done, making an erroneous change so it compiles and performs a call using illegal values. Therefore, the "problem" you are reporting is a coding error on your part, not an issue with the definition of FormatMessage, which hasn't changed since it was created. So Microsoft will correctly ignore your complaint, since it is clear that you failed to read the documentation and wrote bad code.

The declaration should NOT be changed as you indicate; it should remain as
LPTSTR buffer. Certainly, your code that does not cast is simply erroneous code. If you change the declaration as you have shown, your code is now nonfunctionally incorrect, because you are passing a NULL pointer to the buffer.
You have additionally said that the pointer is NULL and the size of the buffer is 0, which means that FormatMessage will NOT return any data. If you had set the size to > 0, you would take an access fault because you passed a NULL pointer to the buffer. You must either pass a valid buffer pointer and a valid size, or a *pointer* to an LPTSTR and use FORMAT_MESSAGE_ALLOCATE_BUFFER and free it with LocalFree. That's why the value has to be an LPTSTR, and why you have to do the cast (and that's why I wrote my code the way I wrote it).

You would NOT declare va_list * args = NULL; that is plain silly. If you want NULL, you use NULL; you don't need to declare a variable for that purpose. If you want to pass 0 for the nSize, you write a 0 in that parameter position; it is extremely silly to declare a variable, initialize it to 0, and use the name of the variable. What part of "constant" have you failed to understand in the C language? If you want the default language, you would use LANG_USER_DEFAULT or some other constant (or 0), but to declare a variable for the sole purpose of setting it to a constant value so you can use a variable name in the call is absolutely absurd.

Perhaps the reason you feel compelled to write long variable names is a proclivity to use unnecessary variable declarations that serve no useful purpose. So there is no need to use variable names at all. The purpose of the declarations in the description is to tell you the TYPE of the argument, not to insist that there be a variable of that name. It is a design error to declare tons of variables that serve no useful purpose. And I would NEVER use the names from the documentation, because they all use the silly "Hungarian Notation", which is one of the worst ideas to have been foisted off on the world since FORTRAN decided that all variables that started with I through O would be integers.

In addition, you must NEVER, EVER, UNDER ANY IMAGINABLE CIRCUMSTANCES, use the m_ prefix for a parameter or local variable. If you are going to adopt these meaningless and burdensome conventions, you are OBLIGED to use them correctly, and m_ means it is a class member. You declared these as local variables, and in that case, m_ is completely and absolutely incorrect by ANY standard of programming. If they *are* class members, your coding style needs a recalibration, so I'm assuming they are, as they should be, local variables, and therefore must NOT be called m_.

If you want to emulate poor programming styles, understand them first. If you don't understand them, don't try to follow them because you are not actually following the styles, just blindly copying naming conventions with no understanding, and getting them completely wrong.

I would have written that call much more simply--and correctly-- as

DWORD result = FormatMessage( FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_ALLOCATE_BUFFER,
NULL,
msgid,
LANG_USER_DEFAULT,
(LPTSTR)&lpBuffer,
0, NULL);


I don't need to use fancy names for temporary variables that have a lifetime of a few lines of code. I don't need to declare variables just to pass a constant to a function.

All strings are called s. All formatting strings are called fmt. All temporary messages are called msg. If you limit the scope, you don't need to worry about conflicts. The smaller the scope of your declaration, the less you need to worry about the significance of the name.

In C++, you do not declare all the variables at the top of the block; this is actually poor style. You don't declare the variable until you need it.

The best thing you can do about any style Petzold teaches is to unlearn it as quickly as possible. I can always tell someone who learned from Petzold: their code is really poor, and the organization of their program is abysmal. It took me two years to unlearn all the style errors that Petzold teaches. both at the lines-of-code level and the program-organization level.

Using a variable which has the same name as the documentation means that you can't do two calls of the same API in the same scope with different arguments, which clearly makes no sense. I can't figure out how it could EVER make sense.

You would typically write NULL for the last argument of FormatMessage. If you don't, it is either a pointer to a va_list of arguments that will be used to provide parameters for the %1, %2, etc. formatting requests in the formatting string, or a pointer to an array of LPTSTRs, depending on the setting of one of the flags.

If you don't use %1, %2, etc. then you don't need the last parameter and you can write NULL for it.

For example

CString FormatMyMessage(UINT id, ...)
{
va_list args;
va_start (args, id);
CString fmt;
LPTSTR s;
fmt.LoadString(id);
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_STRING,
fmt,
0,
LANG_USER_DEFAULT,
(LPTSTR)&s,
0,
args);
CString result(s);
LocalFree(s);
va_end(args);
return result;
}

This is a sketch; error checking is left as an Exercise For The Reader.

IDS_DEVICE_ERROR "There is a problem %1 with device %2"

CString s = FormatMyMessage(IDS_DEVICE_ERROR, ProblemDescription, DeviceName);

Note this call works correctly even if you change the message to say

IDS_DEVICE_ERROR "Device %2 has a problem: %1"
GeneralUse of language id Pin
Member 64293711-Nov-03 18:13
Member 64293711-Nov-03 18:13 
GeneralRe: Use of language id Pin
Joseph M. Newcomer16-Nov-03 11:07
Joseph M. Newcomer16-Nov-03 11:07 
GeneralFormatMessage and %%% Pin
Armen Hakobyan17-Nov-02 9:33
professionalArmen Hakobyan17-Nov-02 9:33 
GeneralRe: FormatMessage and %%% Pin
Joseph M. Newcomer18-Nov-02 5:46
Joseph M. Newcomer18-Nov-02 5:46 
GeneralGood code, bad design Pin
Jim A. Johnson6-Feb-02 6:35
Jim A. Johnson6-Feb-02 6:35 
GeneralRe: Good code, bad design Pin
peterchen6-Feb-02 8:08
peterchen6-Feb-02 8:08 
GeneralRe: Good code, bad design Pin
Jim A. Johnson6-Feb-02 9:01
Jim A. Johnson6-Feb-02 9:01 
GeneralRe: Good code, bad design Pin
Kovalski8-Mar-07 4:50
Kovalski8-Mar-07 4:50 
GeneralRe: Good code, bad design Pin
Joseph M. Newcomer16-Feb-02 11:43
Joseph M. Newcomer16-Feb-02 11:43 
GeneralYou forgot something important Pin
Andreas Saurwein5-Feb-02 0:27
Andreas Saurwein5-Feb-02 0:27 
GeneralRe: You forgot something important Pin
Joseph M. Newcomer16-Feb-02 17:16
Joseph M. Newcomer16-Feb-02 17:16 
GeneralRe: You forgot something important Pin
Andreas Saurwein16-Feb-02 21:55
Andreas Saurwein16-Feb-02 21:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.