Decoding Windows Error Codes — The Three-Layer Structure of Win32 Errors, HRESULT, and NTSTATUS

· · Windows, Error Codes, HRESULT, NTSTATUS, Win32 API, Troubleshooting, Debugging, Windows Development

“The app screen showed error 0x80004005. What does it mean?” — In incident-investigation consultations, this kind of question is a classic. Anyone who has pasted the number from an error dialog straight into a search engine and been met with a flood of unrelated articles — a Windows Update failure, a shared folder that will not connect, a VBA runtime error, a database connection failure — and become more confused has plenty of company.

That happens because 0x80004005 (E_FAIL) is a generic code whose only meaning is “unspecified failure”. The same code is used in countless situations, so searching on the code alone will not reach the cause. On the other hand, a code such as 0x80070005 can, if you know the structure, be decomposed in a few seconds before you search into “Win32 error number 5 = access denied, wrapped as an HRESULT”.

Windows error codes, for historical reasons, form three layers — Win32 error codes, HRESULT, and NTSTATUS — and they are converted across layers. Once you have that structure in your head, you can judge for yourself “which layer whose party returned this code” and “what the essential code is”, and the opening move of an investigation becomes much faster.

Aimed at IT staff in small and medium businesses and at Windows app developers, this article organizes how to tell the three error-code systems apart and how to decompose them, the relationship to .NET exceptions, and practical lookup with err.exe and PowerShell — grounded in Microsoft Learn and the published specification [MS-ERREF] as of August 2026.

1. The Bottom Line First

  • Windows error codes are mainly three systems. Win32 error codes (the small decimal GetLastError returns), HRESULT (the 32-bit code from COM onward, hex starting with 0x8 or a negative decimal), and NTSTATUS (kernel-layer codes; errors start with 0xC).123
  • Decimal and hexadecimal are different notations for the same code. “Error 5”, “0x5”, and “the low 16 bits of 0x80070005” all refer to ERROR_ACCESS_DENIED (access denied).1
  • 0x8007xxxx is “a wrapped Win32 error”. It is a Win32 error code stored in HRESULT FACILITY_WIN32 (7); convert the low 16 bits to decimal and you have the essential code. This is the most important pattern in reading error codes.45
  • 0x80004005 (E_FAIL) is not a cause code. It means “Unspecified failure” and holds no more information. Rather than digging into this code, you should look for the originating context and accompanying logs.6
  • A negative decimal (-2147467259 and the like) is an HRESULT. The most significant bit of the 32 bits (the failure bit) is set, so a signed display is negative. Convert it to hex and then read it.2
  • An 8-digit value starting with 0xC is an NTSTATUS. 0xC0000005 (access violation) and 0xC0000135 (DLL not found) show up constantly in the event log and dumps at crash time. They are unrelated to Win32 error number 5.7
  • The same code changes meaning with context. The cause of error 5 ranges over ACL, elevation, antivirus, a held file, and more, and the “file not found” of error 2 is often a dependent DLL. Always read the meaning of the code together with which API failed against what.1
  • Conversion and lookup tools are standard. certutil -error and net helpmsg are built into Windows; PowerShell’s Win32Exception gets the message; on a development machine err.exe (Microsoft Error Lookup Tool); in dump analysis WinDbg’s !error.8910
  • In .NET, an HRESULT is mapped to an exception type. A known HRESULT goes to the corresponding exception type (E_ACCESSDENIED → UnauthorizedAccessException, and the like); an unknown one becomes a COMException; the original value remains in Exception.HResult.11

In one sentence, the pattern of a Windows error-code investigation is “align the notation to hex → judge which layer the code is → decompose and take out the essential code → read it together with context”.

2. Windows Has Three Error-Code Systems

First, the overall map. Windows error codes split mainly into the following three systems, by the layer that returns them.

System Main party that returns it Typical appearance Representative example
Win32 error code Win32 API (GetLastError), a command’s exit code A small decimal (0–15999) 5 = ERROR_ACCESS_DENIED
HRESULT A COM component, the shell, an installer, many frameworks 8-digit hex starting with 0x8, or a negative decimal 0x80004005 = E_FAIL
NTSTATUS The kernel, a driver, a native API (ntdll) Errors are 8-digit hex starting with 0xC 0xC0000005 = STATUS_ACCESS_VIOLATION

Historically they stacked in this order: Win32 error codes that inherited MS-DOS error numbers, NTSTATUS that the NT kernel uses internally, and HRESULT designed at the introduction of COM to “pack success/failure and the origin into 32 bits”. On current Windows, a conversion flow is everyday: the kernel returns an NTSTATUS, the Win32 subsystem converts it to a Win32 error code, and the COM layer wraps it further as an HRESULT.124

Conversion flow across the three systemsThe Win32 subsystem converts an NTSTATUS the kernel returned into a Win32 error code, and the COM layer wraps it further as an HRESULTThe Win32 subsystem convertsThe COM layer wrapsKernel and driversNTSTATUS (errors are 0xC…)Win32 error code (5 and the like)HRESULT (0x8007xxxx)

Figure 1: The conversion flow across layers. A kernel NTSTATUS becomes a Win32 error, and is wrapped further as an HRESULT.

2.1. Get used to reading decimal and hexadecimal interchangeably

Before you tell the three systems apart, you need to absorb notation wobble. The same code is displayed as decimal or hex depending on the situation.

  • “Error 5”, “error code: 0x5” → the same ERROR_ACCESS_DENIED
  • “Error 1223”, “0x4C1” → the same ERROR_CANCELLED
  • “0x80070005”, “-2147024891” → the same HRESULT

In PowerShell the conversion is one line.

# Decimal → hex
'0x{0:X8}' -f 1223          # 0x000004C1
'0x{0:X8}' -f -2147024891   # 0x80070005 (negative = HRESULT to hex)

# Hex → decimal
0x4C1                        # 1223

When you see a negative decimal that starts with “-214…”, reflexively convert it to hex. That alone cuts a lot of getting lost at the entrance of an investigation.

Three appearances of the same codeDecimal error 5, hex 0x5, and the low 16 bits of 0x80070005 all refer to the same ERROR_ACCESS_DENIEDDecimal notation: error 5ERROR_ACCESS_DENIEDHex notation: 0x5The low 16 bits of 0x80070005Different notation, the same code

Figure 2: Decimal, hex, and the low 16 bits of an HRESULT are only different notations for the same code.

3. Win32 Error Codes — GetLastError and FORMAT_MESSAGE

3.1. Basic GetLastError behavior

Many Win32 APIs such as CreateFile and RegOpenKeyEx indicate failure with the return value (FALSE, NULL, INVALID_HANDLE_VALUE, and the like), and store the detailed error code in a “last-error code” held per thread. The caller retrieves it with GetLastError immediately after confirming failure.13

There are two practical caveats.13

  1. Read it immediately after failure. If you insert another API call (a logging function, for example) in between, that call can overwrite the last-error code.
  2. Do not rely on the value on success. Some APIs clear the last-error code to 0 on success; some do not touch it. The rule is to confirm failure from the return value and then read.
Read GetLastError immediately after failureAfter confirming failure from the return value, retrieve the last-error code with GetLastError immediately, without inserting another API callWin32 APIAppWin32 APIAppInserting another API in between can overwrite itCreateFile callFailure return valueGetLastErrorCode 5

Figure 3: Read the last-error code immediately after failure. Inserting another API call in between can overwrite it.

To get a message string from a code, use FormatMessage with the FORMAT_MESSAGE_FROM_SYSTEM flag.1

#include <windows.h>
#include <stdio.h>

void PrintLastError(const wchar_t* apiName)
{
    DWORD code = GetLastError();   // Call immediately after failure (do not insert another API)
    wchar_t message[512] = L"";
    FormatMessageW(
        FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr, code, 0, message, 512, nullptr);
    wprintf(L"%s failed: %lu (0x%08lX) %s", apiName, code, code, message);
}

Leaving both decimal and hex and the message text in your own app’s log, like this, makes a later investigation one step faster.

Look up a message from a code and leave it in the logSpecify the FORMAT_MESSAGE_FROM_SYSTEM flag to FormatMessage to get the error code's message string, and leave decimal, hex, and the message text in the logError code (example: 5)Get the string with FormatMessageMessage textRecord in the logWrite decimal, hex, and the text together

Figure 4: Convert an error code to a message string with FormatMessage, and leave decimal, hex, and the text together in the log.

3.2. Representative codes that come up constantly in the field

Win32 error codes are defined in the range 0–15999, and Microsoft Learn has a full list.1 Among them, the faces you meet over and over in incident investigation are the following.

Decimal Hex Symbol Meaning
2 0x2 ERROR_FILE_NOT_FOUND The specified file was not found
3 0x3 ERROR_PATH_NOT_FOUND The specified path was not found
5 0x5 ERROR_ACCESS_DENIED Access was denied
32 0x20 ERROR_SHARING_VIOLATION Another process is using it and access is not possible
87 0x57 ERROR_INVALID_PARAMETER The parameter is incorrect
122 0x7A ERROR_INSUFFICIENT_BUFFER The buffer passed is too small
998 0x3E6 ERROR_NOACCESS Invalid access to a memory location
1223 0x4C1 ERROR_CANCELLED The operation was cancelled by the user

Of these, 998 (ERROR_NOACCESS) is not “access denied” but the Win32 expression of a memory access violation, the shape of the NTSTATUS STATUS_ACCESS_VIOLATION discussed later after conversion to the Win32 layer. Watch the confusion with number 5. Also, 1223 (ERROR_CANCELLED) is a code that appears when the user chooses “No” on a UAC elevation dialog, for example — more “it was cancelled” than an error.

Error 998 and 5 are different things998 is a memory access violation, an NTSTATUS access violation converted to the Win32 layer, and differs in meaning from 5, which represents access deniedConverted to the Win32 layerNTSTATUS 0xC0000005Error 998 (ERROR_NOACCESS)Meaning is a memory access violationError 5 (access denied)A permissions problem. A different thing from 998

Figure 5: Error 998 is an NTSTATUS access violation converted to the Win32 layer, a different thing from access-denied 5.

3.3. The same code changes meaning with context

More important than memorizing the table of representative codes is the sense that an error code only tells you “the kind of failure”.

  • Error 5 (access denied): Candidates for the cause range widely — insufficient NTFS ACL, writing a protected area without administrator privileges, a block by antivirus or AppLocker, insufficient privileges on a service account, and so on.
  • Error 2 (file not found): It is not necessarily the file the user specified. A dependent DLL the EXE tried to load implicitly, a settings file seen in the wrong place because of registry redirection (32-bit/64-bit), a path whose environment-variable expansion failed — “which file” was not found is not visible from the code.
  • Error 32 (sharing violation): “Which process is holding it” is the real question, but the code does not tell you that.
The cause of error 5 is decided by contextEven the same access denied has several candidate causes such as insufficient ACL or missing administrator privileges, and you need to identify which API failed against whatError 5(access denied)Which candidate?ACL or admin?Product or service?Insufficient ACLNo administratorSecurity-product blockService privileges lowProcmon: failed target

Figure 6: A code only tells you “the kind of failure”. Error 5 has several candidate causes, and identifying the target is required.

The tool that measures “which API, against which object name, returned which result” is Process Monitor. How to use it is covered in detail in “A Practical Guide to Process Monitor (ProcMon)”. Looking up the meaning of an error code and identifying the target that failed are two wheels of the same cart.

4. HRESULT — Reading the Structure Packed into 32 Bits

4.1. Bit layout

HRESULT is a format that packs success/failure, origin, and a detail code into a single 32-bit value. The published specification [MS-ERREF] defines it with the following layout.2

Bit position Name Meaning
31 S Severity. 0 = success, 1 = failure
30 R Reserved (part of severity when mapping NTSTATUS)
29 C Customer bit. 1 means a code defined by someone other than Microsoft
28 N 1 means an NTSTATUS value mapped into HRESULT space
27 X Reserved (0)
26–16 Facility A facility code that indicates the origin (11 bits)
15–0 Code A detail code within the facility (16 bits)

The most significant S bit is 1, that is, an HRESULT whose hex notation starts at 0x8 or above is a failure. Displaying that as a signed 32-bit integer makes it negative — that is the identity of the “-214…” mentioned earlier.

The relationship between the S bit and a negative displayA failure HRESULT has the most significant S bit set to 1, so in hex it starts at 0x8 or above, and as a signed 32-bit integer it is negativeS bit = 1 (failure)Hex starts at 0x8 or aboveA signed display is negativeWhen you see a negative, convert to hex and read

Figure 7: A failure HRESULT starts at 0x8 or above because the S bit is 1, and a signed display is negative.

Representative Facility values are as follows.5

Facility Value Hex appearance Meaning
FACILITY_NULL 0 0x8000xxxx Widely common codes (E_FAIL, E_UNEXPECTED, and the like)
FACILITY_RPC 1 0x8001xxxx RPC-origin
FACILITY_ITF 4 0x8004xxxx An interface-defined error (meaning depends on the interface)
FACILITY_WIN32 7 0x8007xxxx A wrapped Win32 error code
FACILITY_WINDOWS 8 0x8008xxxx Additional Microsoft-defined interfaces

4.2. Decomposing 0x80004005 and 0x80070005

Let us actually decompose them.

For 0x80004005: S=1 (failure), Facility=(0x80004005 » 16) & 0x7FF = 0 (FACILITY_NULL), Code=0x4005. A generic FACILITY_NULL code, defined as E_FAIL “Unspecified failure”.6 In other words this code holds only the meaning “a failure that cannot report detail”. When you see 0x80004005, stop digging into the code itself there, and shift the investigation’s weight to “which component returned it” and “is there detail in the event log or the app log at the same time”.

For 0x80070005: S=1, Facility=7 (FACILITY_WIN32), Code=0x0005=5. You can see that it is Win32 error number 5 (ERROR_ACCESS_DENIED) wrapped as an HRESULT. The alias E_ACCESSDENIED is, in substance, this value.6

Even for the same “access denied”, 0x80070005 is a wrap of a concrete failure that occurred at the Win32 layer, and the amount of information is completely different from 0x80004005.

Decomposition of 0x80004005 and 0x800700050x80004005 is the generic FACILITY_NULL code E_FAIL, holds no detail, and should move to a context investigation; 0x80070005 is FACILITY_WIN32 and can be seen as a wrap of Win32 error number 5, access denied0x80004005Facility=0 (FACILITY_NULL)Code=0x4005 → E_FAILUnspecified failure. On to a context investigation0x80070005Facility=7 (FACILITY_WIN32)Code=0x0005 → 5ERROR_ACCESS_DENIED

Figure 8: Even the same “failure” has a different amount of information once decomposed. 0x80070005 can be walked to Win32 error number 5.

4.3. The most important pattern: 0x8007xxxx = HRESULT_FROM_WIN32

To convey a failure from a lower layer that can only return a Win32 error code to an upper layer that returns HRESULT (a COM method or the .NET runtime), winerror.h provides the HRESULT_FROM_WIN32 macro.4 The behavior is “store the Win32 error code in the low 16 bits, set Facility to FACILITY_WIN32 (7), and set the S bit to 1”.

How HRESULT_FROM_WIN32 worksStore the Win32 error code in the low 16 bits, set Facility to 7 and the S bit to 1, and assemble a 0x8007xxxx HRESULTWin32 error code (example: 5)Store in the low 16 bitsSet Facility to 7Set the S bit to 10x80070005

Figure 9: HRESULT_FROM_WIN32 stores the Win32 error in the low 16 bits and sets Facility=7 and the S bit.

ERROR_ACCESS_DENIED (5)        --HRESULT_FROM_WIN32-->  0x80070005
ERROR_SHARING_VIOLATION (32)   --HRESULT_FROM_WIN32-->  0x80070020
ERROR_INVALID_PARAMETER (87)   --HRESULT_FROM_WIN32-->  0x80070057 (= E_INVALIDARG)
ERROR_OUTOFMEMORY (14)         --HRESULT_FROM_WIN32-->  0x8007000E (= E_OUTOFMEMORY)

To read the other way, take the low 16 bits in PowerShell.

0x80070005 -band 0xFFFF   # 5 → ERROR_ACCESS_DENIED
0x80072EE7 -band 0xFFFF   # 12007 → ERROR_INTERNET_NAME_NOT_RESOLVED (WinINet)

As in the second example, WinINet and WinHTTP errors (the 12000s) are also defined in Win32 error-code space1, so a networking 0x8007xxxx can be decomposed with the same procedure. Getting “when you see 0x8007, convert the low 4 digits to decimal” into muscle memory is the number-one practical skill this article would like you to take home.

There is the opposite caveat for 0x8004xxxx (FACILITY_ITF). A FACILITY_ITF code has a different party defining the meaning per interface, so the same 32-bit value can mean something different if the party that returned it is different.5 For an unfamiliar 0x8004xxxx, look it up not in a generic search but in the documentation of the component that returned it (a library, a driver SDK, a server product).

How you look it up changes between 0x8007 and 0x8004A FACILITY_WIN32 0x8007xxxx can be read by mechanically decomposing the low 16 bits, but a FACILITY_ITF 0x8004xxxx has a different party defining the meaning per interface, so look it up in the returning component's materials7, WIN324, ITFFacility is?Convert the low 16 bits to decimalMeaning differs by the party that returned itRead it as a Win32 errorLook it up in the returning party's materials

Figure 10: 0x8007xxxx can be decomposed mechanically; 0x8004xxxx is looked up in the returning component’s materials.

5. NTSTATUS — Kernel-Layer Codes and the World of Crashes

5.1. Layout and Severity

NTSTATUS is a 32-bit code used by the kernel, device drivers, and ntdll native APIs, and its layout is similar to HRESULT without being the same.3

Bit position Name Meaning
31–30 Sev Severity. 00 = success, 01 = informational, 10 = warning, 11 = error
29 C Customer bit
28 N Reserved (0, so that a map to HRESULT is possible)
27–16 Facility Facility (12 bits)
15–0 Code Detail code

Because severity is 2 bits, you can read the kind from the leading hex digit. 0xC… is an error (11), 0x8… is a warning (10), 0x4… is informational (01), 0x0–0x3… is success. The breakpoint exception 0x80000003 (STATUS_BREAKPOINT) is a representative example of “a warning, not an error”.37

An NTSTATUS can be read by kind from the leading digitBecause severity is 2 bits, an NTSTATUS can be read as error if the leading hex digit is 0xC, warning if 0x8, informational if 0x4, and success if 0x0 through 0x30xC0x80x40x0–0x3The leading hex digit is?ErrorWarningInformationalSuccessExample: 0x80000003 is a warning

Figure 11: An NTSTATUS can be read by kind from the leading hex digit. 0x80000003 is “a warning, not an error”.

5.2. Where you meet it — exception codes, STOP codes, and the event log

The situations in which IT staff and developers meet NTSTATUS are mainly crash-related.

  • An application-crash exception code: The “Exception code: 0xc0000005” recorded in the event log’s “Application Error (event ID 1000)” is an NTSTATUS. Representative values are as follows.7
Value Symbol Meaning
0xC0000005 STATUS_ACCESS_VIOLATION Access violation (an illegal memory access)
0xC0000135 STATUS_DLL_NOT_FOUND A required DLL was not found and it cannot start
0xC00000FD STATUS_STACK_OVERFLOW Stack overflow
0xC0000374 STATUS_HEAP_CORRUPTION Heap corruption
  • A blue-screen STOP code: They look similar at a glance, but a STOP code (bug check code) is a numbering system of its own, separate from NTSTATUS, such as 0x0000009F (DRIVER_POWER_STATE_FAILURE), and has a dedicated reference.14 Remembering only the distinction “0xC0000005 is NTSTATUS; STOP 0x9F is a bug check code and you must not look it up in an NTSTATUS table” is enough.
  • Process Monitor’s Result column: NAME NOT FOUND and ACCESS DENIED in Procmon’s Result column are display names of the NTSTATUS the kernel returned (STATUS_OBJECT_NAME_NOT_FOUND, STATUS_ACCESS_DENIED). It is also a place where you can feel the layer correspondence of observing a file-I/O failure in NTSTATUS vocabulary and that failure being converted to a Win32 error and arriving at the app.
Telling an exception code from a STOP codeRead an event-log exception code as an NTSTATUS; look up a blue-screen STOP code in the dedicated bug-check-code reference, a different systemException codeSTOP codeWhere did the code appear?Read it as an NTSTATUSLook it up in the bug-check-code tableExample: 0xC0000005Example: 0x0000009F

Figure 12: An event-log exception code is an NTSTATUS; a blue-screen STOP code is a different system. Do not look them up in the wrong table.

Investigation beyond the exception code, that is, capturing and analyzing a crash dump, is covered in “An Introduction to Collecting Windows Crash Dumps” and “Reading Crash Dumps with WinDbg + SOS”.

5.3. The relationship to HRESULT — the N bit and RtlNtStatusToDosError

The bridge between NTSTATUS and the other two layers has two paths.

  1. A map into HRESULT space: Setting the HRESULT N bit (0x10000000) brings an NTSTATUS value into HRESULT space as-is (the HRESULT_FROM_NT macro in winerror.h). Mapping 0xC0000005 becomes 0xD0000005, for example. When you see an HRESULT that starts with 0xD, the correct procedure is to strip the N bit and read it as an NTSTATUS.2
  2. Conversion to a Win32 error code: ntdll’s RtlNtStatusToDosError converts an NTSTATUS to the corresponding Win32 error code. A value with no defined correspondence becomes ERROR_MR_MID_NOT_FOUND.12 For example STATUS_ACCESS_VIOLATION (0xC0000005) is converted to ERROR_NOACCESS (998), and STATUS_OBJECT_NAME_NOT_FOUND (0xC0000034) to ERROR_FILE_NOT_FOUND (2). It is also useful to remember that the kernel’s rich vocabulary is sometimes rounded to a coarser distinction at the Win32 layer.
Two bridges from NTSTATUS to the other layersAn NTSTATUS is passed to the other layers in two ways: mapped into HRESULT space by setting the N bit, and converted to a Win32 error code by RtlNtStatusToDosErrorSet the N bitRtlNtStatusToDosErrorNTSTATUS (0xC0000005)HRESULT (0xD0000005)Win32 error 998 (ERROR_NOACCESS)ERROR_MR_MID_NOT_FOUND if no correspondence is defined

Figure 13: There are two NTSTATUS bridges. A 0xD start is read as an NTSTATUS after stripping the N bit.

6. COM and .NET — How an Error Code Is Mapped to an Exception

6.1. The COM style — HRESULT + IErrorInfo

A COM method fundamentally returns HRESULT, but there is a limit to what can be packed into 32 bits, so as a supplement the IErrorInfo mechanism can convey an error description string and the origin separately. In C++, the compiler-supported _com_error class handles HRESULT and IErrorInfo together. An app whose error dialog shows “code + description” is often carrying the description through this mechanism.

IErrorInfo that supplements HRESULTThere is a limit to what can be packed into a 32-bit HRESULT, so an error description string and the origin are conveyed separately with IErrorInfo, and in C++ the _com_error class handles both togetherHRESULT (32 bits only)There is a limit to what can be packedIErrorInfo carries the description_com_error handles them togetherThe dialog's code + description

Figure 14: A description string that does not fit in a 32-bit HRESULT is carried separately by IErrorInfo.

6.2. The .NET style — from HRESULT to an exception type

When the .NET runtime receives an HRESULT failure in COM interop, it converts it to an exception. A known HRESULT is mapped to the corresponding exception type; an unknown one becomes a COMException.11

Mapping from HRESULT to a .NET exceptionA failure HRESULT received in COM interop is converted to the corresponding exception type if known, or to COMException if unknown, and in either case the original value is kept in Exception.HResultYesNoFailure HRESULTA known mapping?Convert to the corresponding exception typeConvert to COMExceptionThe original value is kept in Exception.HResult

Figure 15: .NET maps an HRESULT to an exception type, and the original value remains in Exception.HResult on every exception.

HRESULT .NET exception type
E_ACCESSDENIED (0x80070005) UnauthorizedAccessException
E_OUTOFMEMORY (0x8007000E) OutOfMemoryException
E_INVALIDARG (0x80070057) ArgumentException
E_NOTIMPL (0x80004001) NotImplementedException
A value with no mapping defined COMException (the original value in the ErrorCode property)

On every exception, the original HRESULT is kept in the Exception.HResult property. A branch in file-I/O exception handling such as “retry only on a sharing violation” can be written with this value.

try
{
    using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None);
}
catch (IOException ex) when (ex.HResult == unchecked((int)0x80070020))
{
    // 0x80070020 = HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION)
    // Another process is holding the file — wait a little and retry, for example
}

6.3. P/Invoke and GetLastError

When you call a Win32 API directly via P/Invoke, specify SetLastError = true on DllImport (or LibraryImport), then retrieve with Marshal.GetLastWin32Error (from .NET 6, the equivalent GetLastPInvokeError). Defining GetLastError itself as a P/Invoke and calling it is inaccurate, because an API call inside the runtime can overwrite the value.15

Retrieving the last error in P/InvokeSpecifying SetLastError as true and retrieving with Marshal.GetLastWin32Error is correct; P/Invoking GetLastError directly is inaccurate because of a runtime overwriteCall a Win32 API via P/InvokeSpecify SetLastError=trueRetrieve with GetLastWin32ErrorA definition that calls GetLastError directlyThe runtime overwrites it and it is inaccurate

Figure 16: In P/Invoke, use SetLastError=true and Marshal.GetLastWin32Error as a set. Calling GetLastError directly is inaccurate.

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern SafeFileHandle CreateFileW(string fileName, uint access, uint share,
    IntPtr security, uint disposition, uint flags, IntPtr template);

// Receive the return value as SafeFileHandle, not IntPtr, and close it reliably with using
// (leaving it as IntPtr leaks a kernel handle)
using var handle = CreateFileW(@"C:\ProgramData\MyApp\config.dat",
    0x80000000 /*GENERIC_READ*/, 0, IntPtr.Zero, 3 /*OPEN_EXISTING*/, 0, IntPtr.Zero);
if (handle.IsInvalid)
{
    int code = Marshal.GetLastWin32Error();              // Example: 5
    var message = new Win32Exception(code).Message;       // Example: Access is denied.
    logger.LogError("CreateFileW failed: {Code} (0x{Code:X8}) {Message}",
        code, code, message);
}

Win32Exception looks up the OS message string from a Win32 error code, so you can use it as-is for leaving both the code and the message in a log. The design question of at which layer to catch an exception and how to leave it in a log is covered in “Where Should catch and Logging Go in Exception Handling?”.

7. Conversion and Investigation Tools in Practice — A Copy-Paste Quick Reference

7.1. err.exe (Microsoft Error Lookup Tool)

A standalone error-lookup tool distributed by Microsoft. It walks a large number of header files such as winerror.h and ntstatus.h and lists definitions and messages that match the specified code.8

err 0x80070005
err 5
err 0xC0000005

A single number can hit in several headers (for example “5” matches definitions in various places besides Win32 ERROR_ACCESS_DENIED), so which of the candidates is plausible must be chosen by context. The download file name is versioned (Err_6.4.5.exe as of this writing), and note also that the code definitions are based on the headers at the time they were bundled.8

Choose err.exe search results by contexterr.exe walks a large number of header files and lists matching definitions, so when several candidates come up for the same number, choose which is plausible by contextEnter err 5Walk a large number of headersSeveral definitions hitChoose a plausible candidate by context

Figure 17: err.exe is a cross-header search, so several candidates can come up, and the plausible one is chosen by context.

7.2. Commands built into Windows

What you can use with no extra install are certutil and net helpmsg. certutil’s -error option displays the message text that corresponds to an error code, and accepts either a hexadecimal HRESULT or a decimal.9

certutil -error 0x80070005
certutil -error 5
net helpmsg 5

net helpmsg is for a Win32 error code in decimal only, but in a Japanese environment the message comes back in Japanese, so you can use it as-is for an explanation to the user.

How to choose among the standard commandsA decimal Win32 error code can be looked up with net helpmsg; a code that includes hex, including HRESULT, can be looked up with certutil's -error optionDecimal Win32Includes hexThe code you have is?net helpmsgcertutil -errorA Japanese message comes backAccepts both hex and decimal

Figure 18: How to choose among the standard commands. A decimal Win32 error is net helpmsg; if it includes hex, certutil -error.

7.3. A collection of PowerShell one-liners

# Win32 error code → OS message string
[System.ComponentModel.Win32Exception]::new(5).Message
# → Access is denied.

# Negative decimal → hex notation (confirm the identity of an HRESULT)
'0x{0:X8}' -f -2147467259     # 0x80004005

# 0x8007xxxx → the Win32 error code in the low 16 bits
0x80070005 -band 0xFFFF        # 5

# HRESULT → confirm the exception .NET maps
[System.Runtime.InteropServices.Marshal]::GetExceptionForHR(-2147024891)
# → UnauthorizedAccessException (0x80070005)

# Win32 error code → HRESULT (reproduce the wrap)
'0x{0:X8}' -f (0x80070000 -bor 32)   # 0x80070020

7.4. WinDbg’s !error

To look up a code during dump analysis, WinDbg’s !error extension is quick. By default it interprets as a Win32 error code; pass 1 as the second argument and it interprets as an NTSTATUS.10

0:000> !error 5
Error code: (Win32) 0x5 (5) - Access is denied.

0:000> !error 0xc0000005 1
Error code: (NTSTATUS) 0xc0000005 - <Access violation>

In a crash dump, !analyze -v automatically displays the exception code (NTSTATUS), so the flow is to confirm the meaning from there with !error <code> 1.

Flow of confirming an exception code in WinDbgIn a crash dump the analyze command automatically displays the exception code; pass that code to the error extension with a second argument of 1 and confirm the meaning as an NTSTATUSOpen the crash dumpRun !analyze -vThe exception code is displayedConfirm the meaning with !error code 1

Figure 19: In dump analysis, look up the exception code that !analyze -v displayed with !error and flag 1.

8. An Investigation Procedure — From Judging the Layer to Matching Against Context

Assemble the knowledge so far into a procedure for actually investigating an error code.

  1. Normalize the notation. If it is a negative decimal, convert it to 8-digit hex. Zero-pad hex shorter than 8 digits and read it.
  2. Judge which layer the code is. As in the judgment table below, the leading few digits almost decide it.
  3. Decompose and take out the essential code. A mechanical operation: the low 16 bits if 0x8007xxxx, strip the N bit if 0xDxxxxxxx.
  4. Look up the name and definition with a tool. Confirm the symbol name and message with err.exe, certutil, or !error.
  5. Match it against context. Identify which app, which operation, which API, failed against what, from the app log, the event log, and Procmon. The code is “the kind of failure”; context is “the place of the cause”.
Procedure for investigating an error codeThe investigation pattern of aligning the notation to hex, judging the layer from the leading few digits, decomposing and taking out the essential code, looking up the name and definition with a tool, then matching against contextDecimalHex0x80070xC / 0xD0xC0xDNormalize to hexLeading digits?8 digits if negativeRead as Win32 errorWhich hex prefix?Low 16 bits → decimalNTSTATUS family?Read as NTSTATUSStrip N bit and readLook up name/definitionMatch context(Procmon)

Figure 20: The investigation pattern. Normalize the notation, judge the layer and decompose, look up the name, then match against context.

Appearance First candidate How to decompose and convert
A 1-to-5-digit decimal (5, 1223, and the like) Win32 error code As-is to net helpmsg or err.exe
A negative decimal (-2147024891 and the like) HRESULT Convert to 8-digit hex, then the judgment of the rows below
0x8007xxxx HRESULT (FACILITY_WIN32) Convert the low 16 bits to decimal and read as Win32
0x8004xxxx HRESULT (FACILITY_ITF) Look it up in the returning component’s documentation
0x8000xxxx HRESULT (FACILITY_NULL) A generic code such as E_FAIL. Shift the weight to a context investigation
0xCxxxxxxx NTSTATUS (error) !error <code> 1; convert to Win32 and read if needed
0xDxxxxxxx An NTSTATUS HRESULT map Strip the N bit (0x10000000) and read as an NTSTATUS
A facility of its own such as 0x8024xxxx An HRESULT specific to a feature area Identify the area from the Facility value and go to dedicated materials (0x8024… is Windows Update)2

What is especially effective in step 5’s “matching against context” is Process Monitor’s Result column. Even if the app only shows “0x80070002”, Procmon tells you in one row “which process, against which path, was returned NAME NOT FOUND”. For looking things up on the event-log side, see also “An Introduction to Windows Event Log and ETW”.

9. Common Misreadings — Patterns That Send an Investigation the Long Way Around

Finally, misreading patterns that show up in actual consultations.

Misreading 1: Thinking 0x80004005 is “a code that indicates a specific cause”

E_FAIL is “Unspecified failure”, and the same value appears in Windows Update, networking, and databases. Trying every remediation that comes up when you search on this code is almost certainly the long way around. Narrow not from the code but from “which app, which operation, other logs at the same time”.6

Misreading 2: Not noticing that a negative decimal is an HRESULT

A case of searching a log that says “Error -2147467259 occurred” as-is, or being confused by “a minus error?”. When you see a negative, convert it to hex. That alone tells you it is 0x80004005 (E_FAIL), and connects to the knowledge of misreading 1.

Misreading 3: Looking up the whole 8 digits of 0x8007xxxx and not looking at the underlying Win32 error

The essence of 0x80070005 is “5 = access denied”. Thinking about “what Win32 error 5 means in the context of this operation” after taking out the low 16 bits reaches the core faster than searching on the whole 8 digits.

Misreading 4: Assuming “the same code = the same cause”

If you once had “error 5 was caused by antivirus”, you tend to jump to the same remediation on the next error 5. Even with the same code, if the API that failed and the target resource differ, the cause is a different thing. Confirming the meaning of the code and identifying the target with Procmon or similar are a set, every time.

Misreading 5: Confusing Win32 error 5 with 0xC0000005, and a STOP code with NTSTATUS

Treating ERROR_ACCESS_DENIED and STATUS_ACCESS_VIOLATION as the same because of the “5” connection sends the investigation in completely different directions — a permissions problem versus a program bug. Also, a blue-screen STOP code is a different system from NTSTATUS, so looking up 0x9F in an NTSTATUS table does not produce a meaningful answer.14

Error 5 and 0xC0000005 have different investigation directionsWin32 error 5 should be investigated as a permissions problem, and NTSTATUS 0xC0000005 as a program bug; treating them as the same sends the investigation in a different directionWin32 error 5Investigate a permissions problemNTSTATUS 0xC0000005Investigate a program bugUnrelated codes in different systems

Figure 21: Do not treat them as the same because of the “5” connection. Error 5 goes toward a permissions problem; 0xC0000005 toward a program bug.

10. Summary

  • Windows error codes are a three-layer structure of Win32 error codes, HRESULT, and NTSTATUS. First judge which layer whose party returned the code.
  • Notation wobble (decimal / hex / negative) can be aligned mechanically. Convert a negative to 8-digit hex and then read it.
  • HRESULT is a structure of S/R/C/N/X bits + Facility (11 bits) + Code (16 bits), and 0x8007xxxx is the most important pattern, a wrapped Win32 error. Convert the low 16 bits to decimal and take out the essential code.
  • A generic code such as 0x80004005 (E_FAIL) does not indicate a cause. The judgment to stop digging into the code and switch to a context investigation is possible precisely because you know the structure.
  • You meet NTSTATUS as a crash exception code or in Procmon’s Result column. 0xC0000005 is an access violation, unrelated to Win32 error 5. A STOP code is yet another system.
  • In .NET, an HRESULT is mapped to an exception type, and the original value remains in Exception.HResult. In P/Invoke, use SetLastError=true and Marshal.GetLastWin32Error as a set.
  • The lookup tools are certutil -error and net helpmsg (standard), err.exe (a development machine), PowerShell one-liners, and WinDbg’s !error.
  • The procedure is “normalize the notation → judge the layer → decompose → look up the name → match against context”. What a code tells you is the kind of failure; the place of the cause is what context tells you.

The next time you meet an unfamiliar error code, look at the leading few digits before you paste it into the search box. The low 4 digits if 0x8007, NTSTATUS if 0xC, convert to hex if negative — this 10-second decomposition largely decides the investigation time that follows.

KomuraSoft LLC handles incident investigation that starts from an error code — “I don’t know what this error code means”, “0x80070005 only appears in a specific environment” — error-handling design for apps that mix Win32 API, COM, and .NET, and identifying causes with crash dumps and Process Monitor. A consultation from a single screenshot of an error dialog is fine.

References

  1. Microsoft Learn, Debug system error codes. An index to the list of Win32 system error codes (0–15999); getting the message for a code GetLastError returns with FormatMessage and the FORMAT_MESSAGE_FROM_SYSTEM flag; that WinINet/WinHTTP errors (the 12000s) are defined in this space; and investigation methods with the Microsoft Error Lookup Tool and the !err command.  2 3 4 5 6

  2. Microsoft Open Specifications, [MS-ERREF]: HRESULT. The HRESULT bit layout (S, R, C, N, and X bits, 11-bit Facility, 16-bit Code); that the N bit indicates an NTSTATUS value mapped into HRESULT space; and the list of facility codes including FACILITY_WINDOWS_UPDATE (36).  2 3 4 5

  3. Microsoft Open Specifications, [MS-ERREF]: NTSTATUS. The NTSTATUS bit layout (2-bit Sev, C bit, N bit, 12-bit Facility, 16-bit Code); and that severity splits into four kinds: success (00), informational (01), warning (10), and error (11).  2 3

  4. Microsoft Learn, HRESULT_FROM_WIN32 macro. The definition of the winerror.h macro that maps a Win32 system error code to an HRESULT value.  2 3

  5. Microsoft Learn, Structure of COM Error Codes. The role of the HRESULT severity bit and the facility field; the values of FACILITY_NULL, FACILITY_RPC, FACILITY_ITF, FACILITY_WIN32, and FACILITY_WINDOWS; and that a FACILITY_ITF code has its meaning defined per interface and the same value can mean something different.  2 3

  6. Microsoft Learn, Common HRESULT values. That E_FAIL (0x80004005) is “Unspecified failure”; and definitions of frequently seen HRESULT values such as E_ACCESSDENIED (0x80070005), E_INVALIDARG (0x80070057), and E_OUTOFMEMORY (0x8007000E).  2 3 4

  7. Microsoft Open Specifications, [MS-ERREF]: NTSTATUS values. The list of NTSTATUS values including STATUS_ACCESS_VIOLATION (0xC0000005), STATUS_DLL_NOT_FOUND (0xC0000135), STATUS_STACK_OVERFLOW (0xC00000FD), STATUS_HEAP_CORRUPTION (0xC0000374), and STATUS_BREAKPOINT (0x80000003).  2 3

  8. Microsoft Learn, The Microsoft Error Lookup Tool. That it is a standalone tool that displays the message text associated with a hexadecimal status code across various header files such as Winerror.h; that the download file name is Err_6.4.5.exe; and that you need to note that the bundled definitions are as of compile time.  2 3

  9. Microsoft Learn, certutil. That certutil’s -error option displays the message text associated with an error code, and that an error notation that includes a symbol name is used in a form such as 0x80070002 (WIN32: 2 ERROR_FILE_NOT_FOUND).  2

  10. Microsoft Learn, !error. That WinDbg’s !error extension decodes and displays Win32, Winsock, NTSTATUS, and NetAPI error values; and that specifying 1 as the flag interprets as an NTSTATUS.  2

  11. Microsoft Learn, How to: Map HRESULTs and exceptions. The mutual-mapping mechanism between COM HRESULT and .NET exceptions; the correspondence table such as E_NOTIMPL → NotImplementedException; that an HRESULT with no explicit mapping is converted to COMException; and that exception Message, Source, and the like are initialized from IErrorInfo information.  2

  12. Microsoft Learn, RtlNtStatusToDosError function (winternl.h). That it is a function that converts an NTSTATUS code to the corresponding Win32 system error code; that ERROR_MR_MID_NOT_FOUND is returned when no correspondence is defined; and that a function that performs the reverse conversion does not exist.  2

  13. Microsoft Learn, Last-Error Code. That the last-error code is held per thread; that it should be retrieved with GetLastError immediately after failure; that APIs that overwrite the code with 0 on success and APIs that do not are mixed; and that bit 29 is reserved for application-defined codes.  2

  14. Microsoft Learn, Bug check code reference. The list of bug check codes (STOP codes) displayed on a blue screen, and how to display information about a code with WinDbg’s !analyze extension. That it is a numbering system of its own, separate from NTSTATUS, can be confirmed from the list.  2

  15. Microsoft Learn, Marshal.GetLastWin32Error Method. That it is a way to retrieve the last-error code of a P/Invoke call that set the SetLastError flag; that P/Invoking GetLastError directly is not trustworthy because of overwrite by an API call inside the runtime; and that from .NET 6 GetLastPInvokeError is recommended. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

What does error 0x80004005 mean?
0x80004005 is the HRESULT E_FAIL, and it means "Unspecified failure". In other words it only indicates that "a failure occurred that cannot report a detailed reason"; it is not a code that represents the cause itself. The same 0x80004005 appears in unrelated places — networking, Windows Update, VBA, a database driver — for this reason. When you see this code, do not dig into the meaning of the code; narrow the cause from the context of which app and which operation produced it, and from other error information left in the event log or a detailed log.
What is a negative error code such as -2147467259?
It is a 32-bit HRESULT displayed as a signed decimal. An HRESULT sets the most significant bit on failure, so as a signed integer it is always negative. In PowerShell, '0x{0:X8}' -f -2147467259 converts it back to hexadecimal (in this example 0x80004005 = E_FAIL). When you see a negative number starting with -214… in a log or a script error message, the standard first step is to convert it to hex and then look it up.
What is the easiest way to look up the meaning of an error code?
With no extra install you can use net helpmsg 5 at the command prompt (for a Win32 error in decimal) and certutil -error 0x80070005. certutil also accepts a hexadecimal HRESULT and displays the symbol name and the message text. In PowerShell, [System.ComponentModel.Win32Exception]::new(5).Message gets the localized message. On a development machine, keep Microsoft's official error lookup tool err.exe (Microsoft Error Lookup Tool); it can search across Win32, HRESULT, and NTSTATUS and list matching definitions in one go.
What kind of error is 0xC0000005?
It is the NTSTATUS STATUS_ACCESS_VIOLATION, that is, an access violation (an illegal memory access). It is the code you see most often as the "Exception code" in the event log or in a crash dump when an application crashes, and it indicates a program bug such as a dereference of an invalid pointer or access to already-freed memory. It looks similar in name to Win32 error 5 (ERROR_ACCESS_DENIED = access denied), but it is an unrelated code in a different system, so do not confuse them. The reliable way to identify the cause is to capture a crash dump and analyze it in WinDbg.
Why is the cause different every time, even with the same error code?
Because an error code only represents "what kind of failure", and "what failed and why" is decided by the calling context. Error 5 (access denied), for example, is the same code for completely different causes — insufficient NTFS permissions, missing administrator privileges, an antivirus block, and so on. In a similar situation, if another process still has the file open you get a different code (error 32 = sharing violation), and reading the code correctly changes where you look. Error 2 (file not found) is also often not the main file but a dependent DLL or a settings file. Once you have looked up the meaning of the code, confirming which API failed against which resource with Process Monitor or similar is the short path to identifying the cause.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog