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

· Updated: · · Windows, Error Codes, HRESULT, NTSTATUS, Win32 API, Bug Investigation, Debugging, Windows Development

Revision history (first version, published Aug 20, 2026)
First published
Cite this article(DOI (registered archive): 10.5281/zenodo.22170857)

The DOIs below refer to previously archived versions and may not match the current text. Use this page’s URL to reference the current text.

Go Komura (2026). Decoding Windows Error Codes — The Three-Layer Structure of Win32 Errors, HRESULT, and NTSTATUS. KomuraSoft LLC. https://comcomponent.com/en/blog/windows-error-codes-win32-hresult-ntstatus/

DOI (registered archive)
10.5281/zenodo.22170857
DOI (last registered version)
10.5281/zenodo.22170858

“The app showed error 0x80004005. What does it mean?” This is a classic question in bug investigations. Search for the number as it is, and you get remedies for unrelated situations: Windows Update, shared folders, VBA, database connections.

The search results scatter because 0x80004005 (E_FAIL) is a generic code that means nothing more than “a failure with unknown details.” 0x80070005, by contrast, can be decomposed into “Win32 error number 5 = access denied, rewrapped as an HRESULT.” Codes that look alike carry different amounts of information.

Windows has three systems, Win32 error codes, HRESULT, and NTSTATUS, and a code is converted when it crosses a layer. To speed up an investigation, rather than memorizing numbers, it is more effective to tell “who returned it, in which layer” and “what the code was before it was rewrapped.”

This article is a practical guide for IT staff at small and medium-sized businesses and for Windows application developers. Based on Microsoft Learn and the public specification [MS-ERREF] as of August 2026, it connects, in order, how to tell the three systems apart, how to decompose an HRESULT, the relationship to .NET exceptions, and how to look codes up with err.exe and PowerShell.

1. The Conclusion First

The first things to grasp are the following three points.

  1. Normalize the notation and identify the code system. Win32 error codes, HRESULT, and NTSTATUS are separate systems. Decimal and hexadecimal are different notations for the same value, and an HRESULT is sometimes displayed as a signed negative number. First bring the value into 8 hex digits, then read it together with the API that returned it and the place where it appeared.123
  2. Decompose 0x8007xxxx; for E_FAIL, move on to investigating the context. 0x80070005 is an HRESULT with FACILITY_WIN32 (7), and its low 16 bits are 5 = ERROR_ACCESS_DENIED. 0x80004005 (E_FAIL), on the other hand, is an “unspecified failure,” and the code alone cannot narrow down the cause.456
  3. Look up the meaning of the code together with the operation and target that failed. The same error 5 has a range of causes: ACLs, elevation, security software, and more. The target of an error 2 may be a dependent DLL, and a file held by another process may show up as error 32. Once you have the name, move on to logs and Process Monitor to find “which API failed on what.”1

For tools, use Windows’ built-in certutil -error and net helpmsg, PowerShell’s Win32Exception, err.exe on a development machine, and WinDbg’s !error during dump analysis, each where it fits.789 Even when the error has already become a .NET exception, the original HRESULT can be traced through Exception.HResult.10

Where to read, by goal

What you want to know Section to read
Identify the code in front of you and look it up right away Normalize the notation in Chapter 2, then go to the commands in Chapter 7 and the procedure in Chapter 8
Log Win32 API failures correctly GetLastError and FormatMessage in Chapter 3, and P/Invoke in Section 6.3
Understand the relationship between 0x80004005, 0x80070005, and .NET exceptions HRESULT decomposition in Chapter 4, and COM and .NET in Chapter 6
Look up crash codes such as 0xC0000005 NTSTATUS and STOP codes in Chapter 5, and WinDbg in Section 7.4
Avoid the common mix-ups in an investigation The misreadings in Chapter 9

In one sentence, the pattern for investigating Windows error codes is “normalize the notation to hex, determine which layer the code belongs to, decompose it to extract the essential code, and read it together with the context.”

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (18 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

2. Windows Has Three Error-Code Systems

First, the big picture. Even for the same numeric value, how you read it depends on which system it was handed over in. Lining up the main sources and the typical appearance gives the following table.

System Main source Typical appearance Representative example
Win32 error code Win32 APIs (GetLastError), command exit codes A small decimal number (0 to 15999) 5 = ERROR_ACCESS_DENIED
HRESULT COM components, the shell, installers, many frameworks 8 hex digits starting with 0x8, or a negative decimal 0x80004005 = E_FAIL
NTSTATUS Kernel, drivers, native APIs (ntdll) Errors are 8 hex digits starting with 0xC 0xC0000005 = STATUS_ACCESS_VIOLATION

The “appearance” in this table is a clue for finding the system. As with the decision table in Chapter 8, use it as a first candidate and cross-check it against what you know about the API or component that returned the code.

Historically, the layers piled up: Win32 error codes, which inherit their numbers from MS-DOS; NTSTATUS inside the NT kernel; and HRESULT, designed when COM was introduced to pack success or failure and the origin into 32 bits.

In current Windows, the conversion kernel NTSTATUS → Win32 error code → HRESULT in the COM layer happens routinely. Tracing back from the code seen in an upper layer to the lower layers is the basis of an investigation.114

The flow of conversion across the three systemsThe Win32 subsystem converts the NTSTATUS returned by the kernel into a Win32 error code, and the COM layer rewraps it again as an HRESULTConverted by the Win32 subsystemRewrapped by the COM layerKernel and driversNTSTATUS (errors are 0xC...)Win32 error code (5, etc.)HRESULT (0x8007xxxx)

Figure 1: The flow of conversion across layers. The kernel’s NTSTATUS becomes a Win32 error, which is then rewrapped as an HRESULT.

2.1. Get Used to Converting Between Decimal and Hexadecimal

Before investigating the system, eliminate the variation in notation. The same code is displayed as decimal, as hexadecimal, or as a signed negative number.

Displayed value The same value in another notation Meaning
Error 5 0x5 ERROR_ACCESS_DENIED
Error 1223 0x4C1 ERROR_CANCELLED
0x80070005 -2147024891 The same HRESULT

In PowerShell, one line converts the notation.

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

# Hex → decimal
0x4C1                        # 1223

When you see a negative decimal starting with “-214…”, reflexively convert it to hex. That alone greatly reduces getting lost at the entrance to an investigation.

Three appearances of the same codeDecimal error 5, hex 0x5, and the low 16 bits of 0x80070005 all point to the same ERROR_ACCESS_DENIEDDecimal notation (error 5)ERROR_ACCESS_DENIEDHex notation (0x5)Low 16 bits of 0x80070005Same code, only the notation differs

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

3. Win32 Error Codes — GetLastError and FORMAT_MESSAGE

3.1. How GetLastError Works

Check the Return Value, Then Save the Last Error

Many Win32 APIs such as CreateFile signal failure through the return value (FALSE, NULL, INVALID_HANDLE_VALUE, and so on) and leave the details in the per-thread “last-error code.” For APIs of this style, call GetLastError immediately after confirming the failure.12

However, check how each API reports its errors. RegOpenKeyEx, for example, does not use the last error; its return value itself is the error code. Treat it separately from the examples that use GetLastError.13

There are two things to watch when using GetLastError.12

  1. Read it immediately after the failure. If another API call (a logging function, for example) comes in between, that call may overwrite the last-error code.
  2. Do not rely on the value after success. Some APIs clear the last-error code to 0 on success, and others leave it untouched. The rule is to confirm the failure through the return value before reading it.
Read GetLastError immediately after the failureOnce the return value shows a failure, get the last-error code with GetLastError right away, without any other API call in betweenWin32 APIAppWin32 APIAppAnother API call in between can overwrite itCreateFile callFailure return valueGetLastErrorCode 5

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

Log the Saved Code Together with Its Message

To get the message string for a code, use FormatMessage with the FORMAT_MESSAGE_FROM_SYSTEM flag. The order is: save the code first, then convert it to a string.1

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

void PrintLastError(const wchar_t* apiName)
{
    DWORD code = GetLastError();   // call immediately after the failure (no other API call in between)
    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);
}

In your own app’s logs, keeping both decimal and hex plus the message text like this makes a later investigation a step faster.

Look up the message for a code and record it in the logPass the FORMAT_MESSAGE_FROM_SYSTEM flag to FormatMessage to get the message string for an error code, and record decimal, hex, and the message text in the logError code (e.g. 5)Get the string with FormatMessageMessage textWrite to the logInclude decimal, hex, and the text

Figure 4: Convert the error code to a message string with FormatMessage, and record decimal, hex, and the text side by side in the log.

3.2. Representative Codes You Meet Constantly in the Field

Win32 error codes are defined in the range 0 to 15999, and Microsoft Learn has the complete list.1 Among them, the ones you meet again and again in bug investigations are the following.

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

The pair that is easy to confuse here is 5 and 998. 998 (ERROR_NOACCESS) is not “access denied”; it is the Win32 expression of a memory access violation. It is what the NTSTATUS STATUS_ACCESS_VIOLATION, described later, looks like after conversion to the Win32 layer.

Also, 1223 (ERROR_CANCELLED) appears when, for example, the user chooses “No” in the UAC elevation dialog. Rather than simply treating it as a malfunction, read it as a code that says the operation was “canceled.”

Error 998 and error 5 are different things998 is the NTSTATUS access violation converted to the Win32 layer, a memory access violation, and its meaning differs from 5, which means access deniedConverted to the Win32 layerNTSTATUS 0xC0000005Error 998 (ERROR_NOACCESS)Means a memory access violationError 5 (access denied)A permission problem. Different from 998

Figure 5: Error 998 is the NTSTATUS access violation converted to the Win32 layer, and is different from error 5, access denied.

3.3. The Same Code Means Different Things in Different Contexts

Memorizing the representative codes does not identify the cause. What a code tells you is only “the kind of failure,” and beyond that something remains to be investigated.

Code What changes with the context What to check next
Error 5 (access denied) Insufficient NTFS ACLs, writing to a protected area without administrator rights, a block by antivirus software or AppLocker, insufficient rights for a service account, and so on Which API was denied access to which target
Error 2 (file not found) Not necessarily the specified file: a dependent DLL that is loaded implicitly, a configuration file that was looked up in a different place because of 32-bit/64-bit registry redirection, a path whose environment-variable expansion failed, and so on “Which file” was not found
Error 32 (sharing violation) Another process is using the target “Which process” is holding it
The cause of error 5 is decided by the contextEven for the same access denied there are several candidate causes such as insufficient ACLs or missing administrator rights, so which API failed on what has to be identifiedError 5 (access denied)Insufficient ACLsNo administrator rightsBlocked by security softwareInsufficient service rightsIdentify the failed target with Procmon

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

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

4. HRESULT — Reading the Structure Packed into 32 Bits

4.1. Bit Layout

An HRESULT is a format that packs success or failure, the origin, and a detail code into a single 32-bit value. Looking first at the S bit for success or failure, Facility for the origin, and Code for the details makes the decomposition examples later easier to follow. The layout in the public specification [MS-ERREF] is as follows.2

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

When the topmost S bit is 1, that is, when the hex notation starts with 0x8 or higher, the HRESULT is a failure. Displayed as a signed 32-bit integer it becomes negative, and that is the identity of the “-214…” mentioned earlier.

The relationship between the S bit and negative displayA failure HRESULT has its topmost S bit set to 1, so in hex it starts with 0x8 or higher, and displayed as a signed 32-bit integer it becomes negativeS bit = 1 (failure)Hex starts with 0x8 or higherNegative in signed displayConvert a negative number to hex before reading

Figure 7: A failure HRESULT has S bit 1, so it starts with 0x8 or higher and is negative in signed display.

The Facility Points to the Next Reference to Consult

The representative Facility values are as follows. Read 7 as a rewrapped Win32 error, 4 as a definition specific to an interface, and so on.5

Facility Value Hex appearance Meaning
FACILITY_NULL 0 0x8000xxxx Broadly shared codes (E_FAIL, E_UNEXPECTED, and so on)
FACILITY_RPC 1 0x8001xxxx Originates from RPC
FACILITY_ITF 4 0x8004xxxx Errors defined by an interface (the meaning depends on the interface)
FACILITY_WIN32 7 0x8007xxxx A rewrapped Win32 error code
FACILITY_WINDOWS 8 0x8008xxxx Additional interfaces defined by Microsoft

4.2. Decomposing 0x80004005 and 0x80070005

Let us split two codes that look alike into S, Facility, and Code and compare them.

Item read 0x80004005 0x80070005
S 1 (failure) 1 (failure)
Facility 0 (FACILITY_NULL) 7 (FACILITY_WIN32)
Code 0x4005 0x0005 = 5
Definition E_FAIL, unspecified failure E_ACCESSDENIED, access denied
Next investigation Investigate the component that returned it and the accompanying logs Investigate the denied operation and target as Win32 error 5

0x80004005 cannot narrow down the cause by the code alone. The Facility is (0x80004005 >> 16) & 0x7FF = 0, the generic FACILITY_NULL code E_FAIL. It carries no detail beyond “Unspecified failure,” so once you have decomposed it, shift the focus of the investigation to “which component returned it” and “whether the event log or the application log at the same time holds details.”6

0x80070005 can be traced to Win32 error 5. Facility = 7 and Code = 5, so it is ERROR_ACCESS_DENIED rewrapped as an HRESULT. The alias E_ACCESSDENIED is the same value.6

Both are “failures,” but decomposition yields different amounts of information. Do not assume E_FAIL means access denied; choose the next investigation according to the value that came back.

Decomposing 0x80004005 and 0x800700050x80004005 is the generic FACILITY_NULL code E_FAIL with no details, so the investigation should move to the context, while 0x80070005 is FACILITY_WIN32 and turns out to be Win32 error 5, access denied, rewrapped0x80004005Facility = 0 (FACILITY_NULL)Code = 0x4005 → E_FAILUnspecified failure. Investigate the context0x80070005Facility = 7 (FACILITY_WIN32)Code = 0x0005 → 5ERROR_ACCESS_DENIED

Figure 8: Both are “failures,” but decomposition yields different amounts of information. 0x80070005 can be traced to Win32 error 5.

4.3. The Most Important Pattern: 0x8007xxxx = HRESULT_FROM_WIN32

Rewrapping a Win32 Failure as an HRESULT

To pass a lower-layer Win32 error on to COM methods and the .NET runtime, which return HRESULTs, winerror.h provides HRESULT_FROM_WIN32. In the failure-code examples below, it puts the Win32 error in the low 16 bits, 7 in the Facility, and 1 in the S bit.4

How HRESULT_FROM_WIN32 worksStore the Win32 error code in the low 16 bits, set the Facility to 7 and the S bit to 1, and assemble a 0x8007xxxx HRESULTWin32 error code (e.g. 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 in Reverse, Extract the Low 16 Bits

To read the original Win32 error from 0x8007xxxx, extract 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 12000 range) are also defined in the Win32 error-code space,1 so network-related 0x8007xxxx codes decompose with the same procedure. Making “when you see 0x8007, convert the low 4 digits to decimal” second nature is the single most practical skill this article hopes you take home.

For 0x8004xxxx, Go to the Documentation of the Component That Returned It

Do not apply the same reading as it is to 0x8004xxxx (FACILITY_ITF). Here the party that defines the meaning differs from interface to interface, so the same 32-bit value can mean different things when it comes from different sources.5

For an unfamiliar 0x8004xxxx, do not settle the matter with a generic search alone; look it up in the documentation of the component that returned it (a library, a driver SDK, a server product).

0x8007 and 0x8004 are looked up differently0x8007xxxx with FACILITY_WIN32 can be read by mechanically decomposing the low 16 bits, but 0x8004xxxx with FACILITY_ITF is defined differently by each interface, so it is looked up in the documentation of the component that returned it7, WIN324, ITFWhich Facility?Convert the low 16 bits to decimalMeaning differs by returning componentRead as a Win32 errorConsult the returning component's documentation

Figure 10: 0x8007xxxx can be decomposed mechanically, but 0x8004xxxx is looked up in the documentation of the component that returned it.

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

5.1. Layout and Severity

NTSTATUS is the 32-bit code used by the kernel, device drivers, and the native APIs in ntdll, and its layout resembles 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, to allow mapping into HRESULT)
27–16 Facility Facility (12 bits)
15–0 Code Detail code

The big difference from HRESULT is that the severity is 2 bits wide and has four kinds: success, informational, warning, and error. From the leading hex digit, for example, 0xC… is an error (11), 0x8… is a warning (10), 0x4… is informational (01), and 0x0… through 0x3… are success.

So do not decide that a value is a failure HRESULT just because it starts with 0x8. The NTSTATUS 0x80000003 (STATUS_BREAKPOINT) is a breakpoint exception, and its severity is “warning,” not “error.”314

The kind of an NTSTATUS can be read from its leading digitBecause the severity is 2 bits wide, an NTSTATUS whose leading hex digit is 0xC is an error, 0x8 a warning, 0x4 informational, and 0x0 through 0x3 success0xC0x80x40x0 to 0x3Leading hex digit?ErrorWarningInformationalSuccesse.g. 0x80000003 is a warning

Figure 11: The kind of an NTSTATUS can be read 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

Let us look at the places where you meet NTSTATUS, divided into exception codes, STOP codes, and Process Monitor.

Exception Codes of Application Crashes

The “Exception code: 0xc0000005” recorded under “Application Error (Event ID 1000)” in the event log is an NTSTATUS. Representative values seen in crashes and startup failures include the following.14

Value Symbol Meaning
0xC0000005 STATUS_ACCESS_VIOLATION Access violation (invalid memory access)
0xC0000135 STATUS_DLL_NOT_FOUND A required DLL was not found, so the program cannot start
0xC00000FD STATUS_STACK_OVERFLOW Stack overflow
0xC0000374 STATUS_HEAP_CORRUPTION Heap corruption

Blue-Screen STOP Codes Are a Separate System

STOP codes (bug check codes) look similar at first glance, but they are a numbering system of their own, separate from NTSTATUS, such as 0x0000009F (DRIVER_POWER_STATE_FAILURE). Use the dedicated reference.15

It is important to separate them as “0xC0000005 is an NTSTATUS, STOP 0x9F is a bug check code,” and never to look a STOP code up in the NTSTATUS table.

In Process Monitor They Appear in the Result Column

NAME NOT FOUND and ACCESS DENIED in Procmon’s Result column are display names for the NTSTATUS values the kernel returned (STATUS_OBJECT_NAME_NOT_FOUND and STATUS_ACCESS_DENIED). Here you can observe file I/O failures in the vocabulary of NTSTATUS and confirm the correspondence between the layers: those values are converted to Win32 errors before they reach the app.

Telling exception codes and STOP codes apartRead an exception code in the event log as an NTSTATUS, and look up a blue-screen STOP code in the dedicated reference for bug check codes, which are a separate systemException codeSTOP codeWhere did the code appear?Read as an NTSTATUSLook up in the bug check code tablee.g. 0xC0000005e.g. 0x0000009F

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

For the investigation beyond the exception code, that is, capturing and analyzing a crash dump, see “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

There are two routes for passing an NTSTATUS on to another system. Think of mapping into HRESULT and conversion to a Win32 error as separate things.

Route What it does Example
Mapping into the HRESULT space HRESULT_FROM_NT sets the N bit (0x10000000) 0xC0000005 → 0xD0000005
Conversion to a Win32 error RtlNtStatusToDosError converts to the corresponding number STATUS_ACCESS_VIOLATION → ERROR_NOACCESS (998)

Mapping into HRESULT sets the N bit and brings the NTSTATUS value into the HRESULT space. The procedure, therefore, is: for an HRESULT starting with 0xD, remove the N bit and read it as an NTSTATUS.2

Conversion to a Win32 error uses ntdll’s RtlNtStatusToDosError. A value with no defined mapping becomes ERROR_MR_MID_NOT_FOUND.11 0xC0000005 is converted to 998, and STATUS_OBJECT_NAME_NOT_FOUND (0xC0000034) to ERROR_FILE_NOT_FOUND (2). Because the kernel side’s rich vocabulary can be rounded into coarser categories in the Win32 layer, read the values before and after conversion as distinct.

The two bridges from NTSTATUS to other layersAn NTSTATUS reaches other layers by two routes, mapping into the HRESULT space by setting the N bit, and conversion to a Win32 error code with RtlNtStatusToDosErrorSet the N bitRtlNtStatusToDosErrorNTSTATUS (0xC0000005)HRESULT (0xD0000005)Win32 error 998 (ERROR_NOACCESS)ERROR_MR_MID_NOT_FOUND if no mapping is defined

Figure 13: There are two bridges out of NTSTATUS. For a value starting with 0xD, remove the N bit and read it as an NTSTATUS.

6. COM and .NET — How Error Codes Map to Exceptions

6.1. The COM Way — HRESULT + IErrorInfo

COM methods return an HRESULT as a rule. There is a limit, however, to what a 32-bit code alone can convey.

The supplement is IErrorInfo. It conveys an error description string and the source separately, and in C++ the compiler-supported _com_error class handles the HRESULT and the IErrorInfo together. In apps whose dialogs show “a code plus a description,” this mechanism is often what carries the description.

IErrorInfo supplements the HRESULTBecause a 32-bit HRESULT has limited room for information, the error description string and the source are conveyed separately through IErrorInfo, and in C++ the _com_error class handles both togetherHRESULT (32 bits only)Limited room for informationIErrorInfo carries the description_com_error handles both togetherCode plus description in a dialog

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

6.2. The .NET Way — From HRESULT to an Exception Type

When the .NET runtime receives a failure HRESULT through COM interop, it converts it into an exception. A known HRESULT is mapped to the corresponding exception type, and an unknown one becomes a COMException.10

Mapping from HRESULT to .NET exceptionsA failure HRESULT received through COM interop is converted to the corresponding exception type if it is known and to COMException if it is not, and in either case the original value is kept in Exception.HResultYesNoFailure HRESULTKnown mapping exists?Convert to the corresponding exception typeConvert to COMExceptionOriginal value kept in Exception.HResult

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

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

Branch Exception Handling on the Original HRESULT

Whatever the exception, the original HRESULT is kept in Exception.HResult. If, for example, you want to retry a file I/O operation “only on a sharing violation,” this value can be the condition. The following example is the part that catches only 0x80070020, which represents a sharing violation.

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 moment and retry, for example
}

6.3. P/Invoke and GetLastError

When calling Win32 APIs directly through P/Invoke, pair the declaration with the retrieval method.

  1. Specify SetLastError = true on DllImport (or LibraryImport).
  2. Immediately after confirming the failure, retrieve the code with Marshal.GetLastWin32Error. On .NET 6 or later, the equivalent GetLastPInvokeError is available.16

Avoid declaring GetLastError itself through P/Invoke and calling it. API calls inside the runtime can overwrite the value, so you may not get the correct failure reason.16

Getting the last error in P/InvokeSetting SetLastError to true and retrieving the code with Marshal.GetLastWin32Error is correct, while calling GetLastError directly through P/Invoke is inaccurate because the runtime overwrites itCall a Win32 API via P/InvokeSpecify SetLastError = trueRetrieve with GetLastWin32ErrorA declaration that calls GetLastError directlyOverwritten by the runtime, inaccurate

Figure 16: In P/Invoke, use SetLastError = true and Marshal.GetLastWin32Error as a pair. 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 a SafeFileHandle, not an IntPtr, and close it reliably with using
// (leaving it as an IntPtr leaks the 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();              // e.g. 5
    var message = new Win32Exception(code).Message;       // e.g. 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 it can be used as it is for leaving both the code and the message in a log. The design question of which layer should catch an exception and how it should be logged is covered in “Where Should catch and Logging Go in Exception Handling?”.

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

First, choose the tool by the environment at hand and your goal. Each of the sections below has examples you can use as they are.

Situation Tool What it can tell you
Check without installing anything certutil -error, net helpmsg The name and message for a code (Section 7.2)
Search definitions across several systems err.exe Candidate definitions in the headers (Section 7.1)
Convert or decompose a value PowerShell Decimal/hex conversion, the low 16 bits, the mapping to a .NET exception (Section 7.3)
Check a meaning during dump analysis WinDbg’s !error Interpretation as Win32 or NTSTATUS (Section 7.4)

7.1. err.exe (Microsoft Error Lookup Tool)

This is a standalone executable error lookup tool distributed by Microsoft. It searches across many header files such as winerror.h and ntstatus.h and lists the definitions and messages that match the specified code.7

err 0x80070005
err 5
err 0xC0000005

When reading the results, watch two things.

  • Choose the candidate that fits the context from among several. “5,” for example, matches definitions other than Win32’s ERROR_ACCESS_DENIED. Do not take the name that matched as the cause without further thought.
  • Check the point in time of the bundled definitions. The download file name carries a version (Err_6.4.5.exe at the time of writing), and the code definitions are based on the headers bundled at that time.7
Choose err.exe results by contextBecause err.exe searches across many header files and lists every matching definition, when several candidates appear for the same number, choose the fitting one from the contextType err 5Search across many headersSeveral definitions matchChoose the fitting candidate from the context

Figure 17: Because err.exe searches across headers, several candidates can appear; choose the fitting one from the context.

7.2. Windows Built-In Commands

certutil and net helpmsg are available without installing anything. certutil’s -error option displays the message text for an error code and accepts both a hex HRESULT and a decimal value.8

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

net helpmsg is only for Win32 error codes in decimal. In a Japanese-language environment the message comes back in Japanese, so it can also be used to explain things to users. To look up a hex HRESULT, choose certutil -error as in the example above.

Choosing between the built-in commandsA decimal Win32 error code can be looked up with net helpmsg, and codes including hex HRESULTs with certutil's -error optionDecimal Win32Includes hexWhat is the code at hand?net helpmsgcertutil -errorMessage comes back in the OS languageAccepts both hex and decimal

Figure 18: Choosing between the built-in commands. A decimal Win32 error goes to net helpmsg; anything that includes hex goes to certutil -error.

7.3. A Collection of PowerShell One-Liners

A quick reference that gathers notation conversion, getting the Win32 message, HRESULT decomposition, and the mapping to a .NET exception. Even after confirming the meaning of a code, the operation and target that failed still need a separate investigation.

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

# Negative decimal → hex notation (reveal the HRESULT behind it)
'0x{0:X8}' -f -2147467259     # 0x80004005

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

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

# Win32 error code → HRESULT (reproduce the rewrapping)
'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 the quick way. By default it interprets the value as a Win32 error code; with 1 as the second argument, it interprets it as an NTSTATUS.9

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 displays the exception code (an NTSTATUS) automatically, so the flow is to confirm its meaning from there with !error <code> 1.

The flow of checking an exception code in WinDbgIn a crash dump the analyze command displays the exception code automatically, and that code is passed to the error extension with 1 as the second argument to confirm its meaning as an NTSTATUSOpen the crash dumpRun !analyze -vThe exception code is displayedCheck the meaning with !error code 1

Figure 19: In dump analysis, take the exception code shown by !analyze -v and look it up with !error and flag 1.

8. The Investigation Procedure — From Determining the Layer to Matching the Context

In an actual investigation, go through the following five stages in order. The point is not to stop at looking up the name, but to confirm, at the end, the operation and target that failed.

  1. Normalize the notation. Convert a negative decimal to 8 hex digits. Zero-pad hex values shorter than 8 digits before reading them.
  2. Determine which layer the code belongs to. Use the table below to narrow down the first candidate from the leading digits, and cross-check it against the API that returned it and the place where it appeared.
  3. Decompose it to extract the essential code. For a 0x8007xxxx HRESULT, read the low 16 bits; for a 0xDxxxxxxx HRESULT, remove the N bit before reading.
  4. Look up the name and definition with a tool. Confirm the symbol name and message with err.exe, certutil, !error, and the like.
  5. Match it against the context. Identify which app, in which operation, had which API fail on what, using application logs, the event log, and Procmon. The code is “the kind of failure”; the context is “where the cause lies.”
The procedure for investigating an error codeThe investigation pattern of normalizing the notation to hex, determining the layer from the leading digits, decomposing to extract the essential code, looking up the name and definition with a tool, and then matching against the contextDecimal notation0x80070xC0xDNormalize the notation (negative numbers to 8 hex digits)Leading digits?Read as a Win32 errorConvert the low 16 bits to decimalRead as an NTSTATUSRemove the N bit and readLook up the name and definition with a toolMatch against the context (Procmon, etc.)

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

Appearance First candidate How to decompose or convert
1 to 5 decimal digits (5, 1223, etc.) Win32 error code Straight to net helpmsg or err.exe
A negative decimal (-2147024891, etc.) HRESULT Convert to 8 hex digits, then apply the rows below
0x8007xxxx HRESULT (FACILITY_WIN32) Convert the low 16 bits to decimal and read as Win32
0x8004xxxx HRESULT (FACILITY_ITF) Look up in the documentation of the component that returned it
0x8000xxxx HRESULT (FACILITY_NULL) Generic codes such as E_FAIL. Shift the focus to the context
0xCxxxxxxx NTSTATUS (error) !error <code> 1, converting to Win32 if needed
0xDxxxxxxx NTSTATUS mapped into HRESULT Remove the N bit (0x10000000) and read as an NTSTATUS
0x8024xxxx and other specific Facilities HRESULT specific to a feature area Identify the area from the Facility value and go to its dedicated reference (0x8024… is Windows Update)2

A concrete example of step 5: from 0x80070002 to the path that was not found

Even when the app shows nothing but “0x80070002,” Procmon’s Result column tells you “which process, on which path, was returned NAME NOT FOUND.” This is the observation that takes you from decomposing the code to identifying the target that actually failed.

For how to look 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, here are the misreading patterns seen in actual consultations.

Misreading 1: Taking 0x80004005 for “a code that indicates a specific cause”

E_FAIL is an “unspecified failure,” and the same value appears in Windows Update, in networking, and in databases alike. Trying, one after another, the remedies that a search for this code turns up is almost certainly the long way around. Narrow down not from the code but from “which app, which operation, and the other logs at the same time.”6

Misreading 2: Not Noticing That a Negative Decimal Is an HRESULT

This is the case of searching for a log line such as “Error -2147467259 occurred” as it is, or being puzzled by “a negative error?” When you see a negative number, convert it to hex. That alone tells you it is 0x80004005 (E_FAIL) and connects you to what Misreading 1 explained.

Misreading 3: Looking Up All 8 Digits of 0x8007xxxx and Missing the Underlying Win32 Error

The essence of 0x80070005 is “5 = access denied.” Rather than searching for all 8 digits, extracting the low 16 bits and asking “what does Win32 error 5 mean in the context of this operation” gets you to the heart of the matter faster.

Misreading 4: Assuming “Same Code = Same Cause”

Once you have experienced “error 5 was caused by antivirus software,” the reflex is to jump to the same fix on the next error 5. Even with the same code, if the failed API and the target resource differ, the cause is something else. Always pair confirming the meaning of the code with identifying the target through Procmon or a similar tool.

Misreading 5: Confusing Win32 Error 5 with 0xC0000005, and STOP Codes with NTSTATUS

If you equate ERROR_ACCESS_DENIED and STATUS_ACCESS_VIOLATION because they share the “5,” the investigation veers off in completely different directions: a permission problem versus a program bug. Also, blue-screen STOP codes are a separate system from NTSTATUS, so looking 0x9F up in the NTSTATUS table gives no meaningful answer.15

Error 5 and 0xC0000005 point the investigation in different directionsWin32 error 5 should be investigated as a permission problem and NTSTATUS 0xC0000005 as a program bug, and equating them sends the investigation in the wrong directionWin32 error 5Investigate a permission problemNTSTATUS 0xC0000005Investigate a program bugUnrelated codes from different systems

Figure 21: Do not equate them because of the shared “5.” Error 5 leads to a permission problem, 0xC0000005 to a program bug.

10. Summary

First, tell the systems apart. The main Windows error codes come in three systems: Win32 error codes, HRESULT, and NTSTATUS. Normalize the variation between decimal, hex, and negative numbers, and confirm who returned the code and in which layer. You meet NTSTATUS in crash exception codes and in Procmon’s Result column, but Win32 error 5 and 0xC0000005 are different things, and STOP codes are yet another system.

Next, read the structure and extract the information you need. An HRESULT consists of the S/R/C/N/X bits, an 11-bit Facility, and a 16-bit Code. The most important pattern is 0x8007xxxx, whose low 16 bits give you the Win32 error. In COM interop, an HRESULT is mapped to a .NET exception type, and the original value remains in Exception.HResult. In P/Invoke, use SetLastError = true and Marshal.GetLastWin32Error as a pair.

Finally, match it against the context. E_FAIL (0x80004005) is not a code that indicates a cause. Once decomposition shows that no more information is coming, stop digging into the code and move on to the operation, the target, and the logs at the same time. For tools, use certutil and net helpmsg, err.exe, PowerShell, and WinDbg’s !error, each where it fits.

The investigation pattern is “normalize the notation → determine the layer → decompose → look up the name → match against the context.” A code tells you only the kind of failure. The next time you meet an unfamiliar number, check its leading digits and where it came from before pasting it into a search box. This first decomposition decides where you look next.

KomuraSoft LLC handles bug investigations that start from an error code, such as “I do not know what this error code means” or “0x80070005 appears only in one particular environment,” the design of error handling in apps that mix Win32 APIs, COM, and .NET, and identifying causes with crash dumps and Process Monitor. A consultation that starts from a single screenshot of an error dialog is welcome.

References

  1. Microsoft Learn, Debug system error codes. The index to the list of Win32 system error codes (0 to 15999), getting the message for a code returned by GetLastError with FormatMessage and the FORMAT_MESSAGE_FROM_SYSTEM flag, the fact that WinINet/WinHTTP errors (the 12000 range) are defined in this space, and how to look codes up with the Microsoft Error Lookup Tool and the !err command. ↩ ↩2 ↩3 ↩4 ↩5

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

  3. Microsoft Open Specifications, [MS-ERREF]: NTSTATUS. The bit layout of an NTSTATUS (the 2-bit Sev, the C bit, the N bit, the 12-bit Facility, and the 16-bit Code), and the four severities: 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 roles of the severity bit and the facility field of an HRESULT, the values of FACILITY_NULL, FACILITY_RPC, FACILITY_ITF, FACILITY_WIN32, and FACILITY_WINDOWS, and the fact that FACILITY_ITF codes have their meaning defined per interface, so the same value can mean different things. ↩ ↩2 ↩3

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

  7. Microsoft Learn, The Microsoft Error Lookup Tool. A standalone tool that displays the message text associated with a hex status code across header files such as Winerror.h, the download file name being Err_6.4.5.exe, and the caution that the bundled definitions are those at compile time. ↩ ↩2 ↩3

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

  9. Microsoft Learn, !error. WinDbg’s !error extension decodes and displays Win32, Winsock, NTSTATUS, and NetAPI error values, and interprets the value as an NTSTATUS when 1 is specified as the flag. ↩ ↩2

  10. Microsoft Learn, How to: Map HRESULTs and exceptions. How COM HRESULTs and .NET exceptions are mapped to each other, the correspondence table such as E_NOTIMPL → NotImplementedException, HRESULTs without an explicit mapping being converted to COMException, and the exception’s Message, Source, and so on being initialized from IErrorInfo information. ↩ ↩2

  11. Microsoft Learn, RtlNtStatusToDosError function (winternl.h). The function converts an NTSTATUS code to the corresponding Win32 system error code, returns ERROR_MR_MID_NOT_FOUND when no mapping is defined, and no function exists for the reverse conversion. ↩ ↩2

  12. Microsoft Learn, Last-Error Code. The last-error code is kept per thread, it should be retrieved with GetLastError immediately after a failure, APIs that overwrite the code with 0 on success coexist with APIs that do not, and bit 29 is reserved for application-defined codes. ↩ ↩2

  13. Microsoft Learn, RegOpenKeyExW function. The return value is ERROR_SUCCESS on success and, on failure, a nonzero error code defined in Winerror.h. ↩

  14. 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

  15. 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. The list confirms that they are a numbering system of their own, separate from NTSTATUS. ↩ ↩2

  16. Microsoft Learn, Marshal.GetLastWin32Error Method. How to get the last-error code of a P/Invoke call whose SetLastError flag is set, why calling GetLastError directly through P/Invoke is unreliable because API calls inside the runtime overwrite it, and that GetLastPInvokeError is recommended on .NET 6 and later. ↩ ↩2

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 its meaning is "Unspecified failure". In other words, it only says that a failure occurred whose detailed reason cannot be reported; it is not a code that represents the cause itself. This is why the same 0x80004005 shows up in unrelated situations such as networking, Windows Update, VBA, and database drivers. When you see this code, do not dig into the meaning of the code; narrow down the cause from the context of which app and which operation produced it, and from the other error information left in the event log or detailed logs.
What is a negative error code such as -2147467259?
It is a 32-bit HRESULT displayed as a signed decimal number. An HRESULT has its most significant bit set to 1 on failure, so displayed as a signed integer it is always negative. Running '0x{0:X8}' -f -2147467259 in PowerShell converts it back to hex notation (0x80004005 = E_FAIL in this example). When you see a negative number starting with -214… in a log or a script error message, the standard move is to convert it to hex first and then look it up.
What is the easiest way to look up the meaning of an error code?
Without installing anything, you can use net helpmsg 5 (for a decimal Win32 error) and certutil -error 0x80070005 at the command prompt. certutil also accepts a hex HRESULT and displays the symbol name and the message text. In PowerShell, [System.ComponentModel.Win32Exception]::new(5).Message returns the message in the OS language. On a development machine, it is convenient to keep Microsoft's official error lookup tool err.exe (Microsoft Error Lookup Tool), which searches Win32, HRESULT, and NTSTATUS definitions in one pass.
What kind of error is 0xC0000005?
It is the NTSTATUS STATUS_ACCESS_VIOLATION, that is, an access violation (an invalid 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 dereferencing an invalid pointer or accessing freed memory. Its name resembles Win32 error 5 (ERROR_ACCESS_DENIED = access denied), but it is an unrelated code from a different system, so do not confuse the two. To identify the cause, the reliable way is to capture a crash dump and analyze it with WinDbg.
Why is the cause different every time even though the error code is the same?
Because an error code only says what kind of failure occurred; what failed and why is determined by the context of the call. Error 5 (access denied), for example, is the same code for completely different causes: insufficient NTFS permissions, missing administrator rights, a block by antivirus software, 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 frequently about a dependent DLL or a configuration file rather than the main executable. Once you have looked up the meaning of the code, the shortcut to the cause is to check with Process Monitor or a similar tool which API failed against which resource.

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