Windows GetLastError Message

Want to show your users a slightly more informative message than “Error 126 happened when doing foo”? How about “Error 126 (The specified module could not be be found) when doing foo”? That’s what this next code sample demonstrates.

A lot of the older “core” Win32 API functions like CreateFile or CreateThread set a thread global error value when they fail to signal an error which can only be checked by GetLastError. This returns a DWORD integer value that can be passed to the venerable FormatMessage function in Windows to get a human readable description of the error.

Before I dive into interesting details for this function, here’s the definition as well as example invocations.

#define NOMINMAX
#include <Windows.h>

#include <algorithm>
#include <array>
#include <string>

/** Returns a message for a Win32 error code. */
std::string getErrorMessage(const DWORD errorCode) {
  // Use FormatMessage to generate an error message from the error code. This
  // uses the 'A' variant because we want to store the message as UTF8 rather
  // than UTF-16.
  // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-formatmessagea
  LPSTR messageBuffer = nullptr;
  const DWORD formatFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER |
                            FORMAT_MESSAGE_FROM_SYSTEM |
                            FORMAT_MESSAGE_IGNORE_INSERTS;

  const auto messageLength = ::FormatMessageA(
      formatFlags,
      nullptr,   // Not using a message location.
      errorCode, // The message identifier to look up.
      0,         // Use default language lookup rules.
      reinterpret_cast<LPSTR>(&messageBuffer), // Receives the allocated buffer.
      0,        // Buffer is zero because the function will allocate a buffer.
      nullptr); // No formatted message arguments.

  // Check if the error code couldn't be converted into a message and return a
  // custom message containing the error code that failed to convert.
  if (messageLength == 0) {
    // Use a stack allocated char buffer to generate the custom message.
    std::array<char, 50> tempBuffer{};

    const auto errorLength = snprintf(
        tempBuffer.data(),
        tempBuffer.size(),
        "::FormatMessage failed with error 0x%x",
        static_cast<unsigned int>(::GetLastError()));

    return std::string{
        tempBuffer.data(), std::min(tempBuffer.size(), static_cast<size_t>(errorLength))};
  }

  // Copy the message to a std::string before freeing the buffer allocated by
  // Windows.
  std::string errorMessage{messageBuffer, messageLength};
  ::LocalFree(messageBuffer);

  // Remove any trailing newline characters from the message before returning.
  while (!errorMessage.empty() &&
         (errorMessage.back() == '\n' || errorMessage.back() == '\r')) {
    errorMessage.erase(errorMessage.end() - 1);
  }

  return errorMessage;
}

/** Returns the last Win32 error as a string message. */
std::string getLastErrorMessage() { return getErrorMessage(::GetLastError()); }

//==============================================================
// Example usage.
//==============================================================
#include <iostream>

int main() {
  std::cout << "0 is: " << getErrorMessage(0) << std::endl;
  std::cout << "126 is: " << getErrorMessage(126) << std::endl;

  // HRESULT works too!
  std::cout << "E_NOTIMPL is: "
            << getErrorMessage(static_cast<DWORD>(E_NOTIMPL)) << std::endl;
}

Some points that I want to call out:

  • FormatMessage like a lot of Windows functions defaults to wide char (FormatMessageW) so we specifically invoke the narrow version by calling FormatMessageA. Note that Windows still converts under the hood, so we still pay the cost of UTF16 -> UTF8 conversion.
  • We pass FORMAT_MESSAGE_ALLOCATE_BUFFER to FormatMessage to let Windows take care of buffer sizing, but we have to be careful here to free that buffer via LocalFree before returning.
  • FormatMessage returns messages that have newline(s) at the end which is (probably) not what the caller expected if they want to insert the error message into a larger message. This function will take care of stripping those characters, and since these characters are at the end of the string .erase is a cheap call.
  • We still want to some something helpful to callers when FormatMessage fails, so this function uses a temporary stack allocated buffer and snprintf to return a message with the error code of why FormatMessage failed.

Also, you can pass an HRESULT value to this function and get a usable message from it as well! Just cast it to a DWORD and you are good to go.