CreateFileW の直後に GetLastError() を読み、3が返ってきた。そんなときにまず疑うのは、末尾のファイル名ではなく、その手前のディレクトリです。3は ERROR_PATH_NOT_FOUND。似たエラーである2(ERROR_FILE_NOT_FOUND)との違いを、実際の戻り値を見ながら切り分けます。
エラー3で最初に見る場所
まず必要なのは、CreateFileW の第1引数へ渡った文字列です。設定画面の値では足りません。途中でフォルダー名を足しているなら、組み立て後のパスを残します。エラー番号は別のAPIを呼ぶ前に保存します。
HANDLE file = ::CreateFileW(path, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE)
{
const DWORD error = ::GetLastError(); // ほかのAPIより先に保存
TRACE(L"CreateFileW failed: path=%s error=%lu\n", path, error);
}
たとえば対象が C:\ProgramData\MyApp\config\app.ini なら、筆者は C:\ProgramData、C:\ProgramData\MyApp、C:\ProgramData\MyApp\config の順に見ます。最初の2つがあり、config だけがなければ、ファイル名を調べる前に原因を絞れます。
コード2と3の違い
Microsoftのシステムエラーコード一覧は、2を「指定されたファイルが見つからない」、3を「指定されたパスが見つからない」と分けています。
| コード | 定数 | 見る場所 |
|---|---|---|
| 2 | ERROR_FILE_NOT_FOUND | 末尾のファイル名、拡張子、ファイルの生成漏れ |
| 3 | ERROR_PATH_NOT_FOUND | 親ディレクトリのうち、最初に存在しなくなる区間 |
Windows 11、Visual Studio 2026、Debug/x64で小さなMFCダイアログを作り、CreateFileW に4種類のパスを渡しました。既存ファイルは0(成功)。存在するフォルダーの中でファイル名だけを変えると2、途中に存在しないフォルダーを挟むと3でした。相対パスは、カレントディレクトリに置いた既存ファイルを開けています。

ただし、2か3だけで「見つからない」系を全部説明できるわけではありません。不正な名前なら123(ERROR_INVALID_NAME)、存在しないドライブなら15(ERROR_INVALID_DRIVE)になることがあります。ここでの2と3の分け方は、今回の CreateFileW と OPEN_EXISTING の結果です。
相対パスが指す場所は実行環境で変わる
data\config.ini は、exeの隣を指すとは限りません。サービス化したあとだけ失敗する、タスクスケジューラから起動したときだけ失敗する、といった場合はカレントディレクトリの違いを疑います。Visual Studioからの起動でも、プロジェクト設定次第で基準位置は変わります。
wchar_t fullPath[32768]{};
const DWORD length = ::GetFullPathNameW(
inputPath, _countof(fullPath), fullPath, nullptr);
if (length == 0)
{
const DWORD error = ::GetLastError();
TRACE(L"GetFullPathNameW failed: error=%lu\n", error);
}
else if (length >= _countof(fullPath))
{
// length は終端の null を含む必要バッファ長
TRACE(L"full-path buffer too small: required=%lu\n", length);
}
else
{
TRACE(L"input=%s full=%s\n", inputPath, fullPath);
}
GetFullPathNameW の結果を元の入力と並べると、「コード上で想定した場所」と「Windowsが解決した場所」のずれが見えます。相対指定が不要なら、設定ファイルの保存先など用途ごとの基準フォルダーを決め、そこから絶対パスを組み立てた方が後で追いやすくなります。
最初に欠けたパス区間の見つけ方
絶対パスが分かったら、末尾のファイル名を外して親ディレクトリを一段ずつ調べます。エクスプローラーで見るだけでも切り分けられますが、ログへ組み込むなら GetFileAttributesW が使えます。
const DWORD attributes = ::GetFileAttributesW(directoryPath);
if (attributes == INVALID_FILE_ATTRIBUTES)
{
const DWORD error = ::GetLastError();
TRACE(L"missing path part: %s error=%lu\n", directoryPath, error);
}
else if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
{
TRACE(L"not a directory: %s\n", directoryPath);
}
GetFileAttributesW が成功しても、FILE_ATTRIBUTE_DIRECTORY がなければ、その名前で存在しているのはフォルダーではありません。実際に、設定値を連結するコードでは「途中の名前と同じファイルがあった」という形もあり得ます。
UNCパスでエラー3になる場合
\\server\share\folder\file.dat では、サーバー名、共有名、共有配下のフォルダーを分けて見ます。サーバー名なら名前解決やVPN、共有名なら共有の変更・廃止、配下ならフォルダーの移動や古い設定値が候補です。
ここでは GetFileAttributesW の癖にも注意が要ります。Microsoftの説明では、共有ルート \\server\share に対する属性取得は失敗します。このAPIで試すなら共有配下からです。また、ネットワークへ到達できないときは53(ERROR_BAD_NETPATH)など別の番号も返ります。UNCだから3、と決め打ちはできません。
まとめ
- 今回の
CreateFileW(OPEN_EXISTING)検証では、親ディレクトリが欠けると3、存在する親の中で末尾のファイルだけがないと2になりました。 - 相対パスは
GetFullPathNameWで絶対化し、APIへ渡した文字列と並べてログへ残します。 - 親ディレクトリを先頭から調べ、最初に欠けた区間と、その場で得たエラー番号を記録します。
