# Design notes: win-error-codes (Tier 1)
Current canonical decisions. The plan is in [CHECKLIST.md](CHECKLIST.md).
**Why the crate exists.** Three crates hold a Win32 error code, and two of them had grown their own
`Win32Error`: [windows-namespace-request-sys](../windows-namespace-request-sys/src/outcome.rs) and
[windows-file-enumeration-sys](../windows-file-enumeration-sys/src/error.rs). They differ, and both
convert a code to `io::Error` wrongly for a code of 2^31 or more. The namespace crate falls back to
`io::Error::other(String)`, so `raw_os_error()` is `None` and its documentation's claim that "the
raw code survives" is false for those codes. The enumeration crate `expect`s that a code fits in an
`i32`, which would panic. Codes that large exist: `windows-sys` defines
`ERROR_VIRTDISK_NOT_VIRTUAL_DISK` as a `WIN32_ERROR` of 3225026581, and a real call returned it. A
third crate, `win-volume-sys`, has none and uses `io::Error`. The same situation produced
[win-shared-os-owned-handle](../win-shared-os-owned-handle/README.md), and this is the same remedy.
## Decision index
| ID | Decision |
|---|---|
| <a id="we-d-1"></a>WE-D-1 | **One type per numbering space, `LStatus` for the signed carrier of one of them, and no distinction between "status" and "code".** The engineer, 2026-10-15. `Win32Error` is a `u32`, the `DWORD` `GetLastError` returns and a call such as `GetStorageDependencyInformation` returns directly. `LStatus` is an `i32`, the `LONG` that registry calls return; it is the same number space and converts to `Win32Error` and back by a bit cast, and it is its own type because it is the signed carrier. `HResult` is an `i32`, signed by definition (`FAILED(hr)` is `hr < 0`), with severity and facility bits. `NtStatus` is an `i32`, the `NTSTATUS` the native API reports in, a third space with a two-bit severity ([WE-D-7](#we-d-7)). The names "status" and "code" are historical and mean the same thing, so no type models a difference between them, and `ERROR_SUCCESS` (0) is an ordinary `Win32Error`. **Not assumed: that a Win32 code fits in 16 bits.** The virtdisk errors do not, so no conversion in this crate masks to 16 bits except `HRESULT_FROM_WIN32`'s own rule. **The `io::Error` boundary is the only place a code becomes an `i32`**, because std's `io::Error::from_raw_os_error` takes and `raw_os_error` returns an `i32`; the cast there is bit-preserving and exact in both directions. **No named constants are defined here**: `windows-sys` carries them. |
| <a id="we-d-2"></a>WE-D-2 | **The name is the engineer's: `win-error-codes`.** The workspace rule on prefixes ([DESIGN-NOTES.md](../../DESIGN-NOTES.md#new-crates-take-the-win-prefix)) gives `win-` to a crate that raises Windows into safe Rust. This crate calls no Windows API; it holds the vocabulary Windows' calls report in, which is the reason it takes the prefix and not a facility name. It carries no `-sys` suffix, because that half of the rule means thin-over-Win32, and this layers over no call. |
| <a id="we-d-3"></a>WE-D-3 | **The numbers build on every target; the `io::Error` bridge is `cfg(windows)`.** Settled in `WE-1.1`, overruleable. The types hold numbers, so nothing about them is Windows-specific and a portable crate (durable-ioring's core is one, [DI-D-49](../durable-ioring/DESIGN-NOTES.md#di-d-49)) can use them. The bridge is not portable in meaning: off Windows `io::Error::raw_os_error` is an `errno`, so `from_io` would read an `errno` as a Win32 code, and that is a wrong answer, which is the ground for removing an option, where a missing feature is not. So `from_io`, `to_io_error`, `last` and the `From` impl are `cfg(windows)`, and the crate is in the repository's portable CI job. A Linux `check` and `clippy` against the installed target pass locally; the portable job is where its tests run there. |
| <a id="we-d-4"></a>WE-D-4 | **An `io::Error` with no OS code is `None`, never zero, and `last` needs no `unsafe`.** Settled in `WE-1.1`, overruleable. Zero is `ERROR_SUCCESS`, so `from_io` returning it for an error that carries no code would make a fabricated error read as a success; the enumeration crate's copy did this. `last` reads `io::Error::last_os_error`, which on Windows is `GetLastError`, so the crate has no `unsafe` and no `windows-sys` dependency outside its tests. The namespace crate's own snapshot-after-the-call guarantee for `perform` stays in that crate and builds the value with `from_code`. |
| <a id="we-d-5"></a>WE-D-5 | **`LStatus` converts to and from `Win32Error` two ways: a `const` inherent method each, and a `From` impl each; it is not an `Error`, and its `Display` is the `Win32Error`'s.** Settled in `WE-1.2`, overruleable. The two are one number space ([WE-D-1](#we-d-1)), so the conversion is a bit cast and loses nothing in either direction, and a negative `LONG` is a code above 2^31, not its magnitude. The inherent methods (`LStatus::to_win32`, `Win32Error::to_lstatus`) are `const` because a `From` impl cannot be; the `From` impls are the form the rest of the workspace will write. `LStatus` does not implement `std::error::Error`, because a value of zero is success and an error type that can be success is wrong: `into_result` is how it becomes a `Win32Error`, which is. Its `Display` delegates, since the signed carrier has no rendering of its own. |
| <a id="we-d-6"></a>WE-D-6 | **`HResult` follows `winerror.h` where the documentation and the header differ, `from_win32` is `HRESULT_FROM_WIN32`'s lossy rule and says so, `win32` is `None` for `S_OK`, and durable-ioring's `ErrorCode` is not changed by this crate.** Settled in `WE-1.3`, overruleable. **Facility:** `facility()` is the 13 bits `HRESULT_FACILITY` takes, not the 11 the documentation draws, because `FACILITY_DXGI` is 0x87A, which needs 12; the reserved bit and the `NTSTATUS` bit are inside the 13, and `is_customer` and `is_ntstatus_mapped` read the bits above. **`from_win32` is not lossless**: zero is `S_OK`; a code that is not positive as an `i32` is taken to be an `HRESULT` already and is returned as it is; any other code keeps only its low 16 bits, so `0x0010045D` and `0x0000045D` give the same value. This is the one place the crate masks to 16 bits ([WE-D-1](#we-d-1)), because the macro does, and it is why there is no `From<Win32Error>`. **`win32` is `None` for `S_OK`**, so the round trip holds for the codes 1 to 65535 and not for zero: a success wraps no error, which is the reading durable-ioring's `ErrorCode` took and tested before its helpers moved. It is `None` for anything but the exact wrapping, so a customer or `NTSTATUS` value that shares the facility is not read as a Win32 code. **Success is any non-negative value**, `S_FALSE` included, so `into_result` cannot tell it from `S_OK` and the raw value is there for a caller who must; `HResult` is not an `Error`, for the reason `LStatus` is not ([WE-D-5](#we-d-5)). **`ErrorCode` is not rebuilt on this type by this crate.** It is a layer-neutral code whose meaning is its reporter's ([DI-D-50](../durable-ioring/DESIGN-NOTES.md#di-d-50)), so changing its public surface is that decision's to make. Its `from_win32` and `win32` were a second copy of this rule, identical on the facts both pinned (the zero case, the low 16 bits, and a value that is not positive passing through), and [DI-9.7.11](../durable-ioring/COMPLETED-CHECKLIST.md#di-9711) settled it by removing them from `ErrorCode`; `win-durable-ioring` builds the same two helpers on `HResult`. |
| <a id="we-d-7"></a>WE-D-7 | **`NtStatus` is `NTSTATUS` with `ntstatus.h`'s own semantics, one conversion that is a macro, and none that is a call.** The engineer, 2026-10-15: "add NtStatus also with the obvious semantics", which retires this file's earlier working position of leaving it out. **Layout:** a two-bit severity, the customer bit, the bit `HRESULT_FROM_NT` sets, a twelve-bit facility, a 16-bit code; `NtSeverity` names the four severities with `ntstatus.h`'s values. **`NT_SUCCESS` is `status >= 0`**, so an informational status is a success and a warning is not; `NT_INFORMATION`, `NT_WARNING` and `NT_ERROR` each name one severity; `into_result` follows `NT_SUCCESS`. **The facility is twelve bits** and excludes the customer bit and the `HRESULT_FROM_NT` bit, unlike `HResult::facility`, whose 13 bits are the macro's. **`HResult::from_ntstatus` is `HRESULT_FROM_NT`** (one bit set) and is lossless only for a status without that bit; **`HResult::ntstatus` is this crate's own inverse**, there being no macro for it, and it clears the bit and is `None` when the bit is clear. **There is no conversion to a Win32 code**: that is `RtlNtStatusToDosError`, an `Rtl*` call the repository's rule on documented interfaces excludes, and a status and a Win32 error of the same number (`STATUS_VIRTDISK_NOT_VIRTUAL_DISK` and `ERROR_VIRTDISK_NOT_VIRTUAL_DISK` are both 0xC03A0015) are separate values in separate spaces. Not an `Error`, for the reason `LStatus` is not ([WE-D-5](#we-d-5)). |