Version
v26.7.0
Platform
Linux x64
Subsystem
fs / src/node_file.cc
What steps will reproduce the bug?
const fs = require('node:fs');
fs.mkdirSync('a/b', { recursive: true });
try {
fs.rmSync('a/.', { recursive: true, force: true });
} catch (err) {
console.log('code :', JSON.stringify(err.code));
console.log('errno :', err.errno);
console.log('syscall:', err.syscall);
console.log('message:', err.message);
}
Output:
code : ""
errno : -4094
syscall: "rm"
message: , Unknown error: Invalid argument 'a/.'
What is the expected behavior? Why is that the expected behavior?
err.code should be 'EINVAL' and err.errno should be -22. The underlying
failure is a plain EINVAL from rmdir(2), which POSIX requires for a path whose
final component is ..
Every other fs error carries a usable code, and err.code === 'EINVAL' is the
documented way to branch on fs failures, so an empty string breaks ordinary error
handling. The message is also malformed: it begins with , because the empty code
is interpolated into it.
What do you see instead?
errno is -4094 (UV_UNKNOWN) and code is the empty string.
Additional information
This is not specific to EINVAL. RmSync() in src/node_file.cc translates the
std::error_code from std::filesystem::remove_all() with a hardcoded chain that
covers four values:
operation_not_permitted -> EPERM
directory_not_empty -> ENOTEMPTY
not_a_directory -> ENOTDIR
permission_denied -> EACCES (EPERM on Windows)
Anything else falls through to the tail of the function:
std::string message = "Unknown error: " + error.message();
return env->ThrowErrnoException(
UV_UNKNOWN, "rm", message.c_str(), path_c_str);
So the mapping is an allowlist rather than a translation, and any errno outside
those four surfaces as UV_UNKNOWN with an empty code. EINVAL is simply the
one that is easiest to trigger from user code.
The async form is unaffected, because fs.rm() and fsPromises.rm() use the JS
rimraf and report EINVAL correctly.
Noting for context that the dot segment handling in #61958 is being addressed
separately at the JS layer. That change stops a/. from reaching this code path,
but it does not fix the mapping, which stays reachable for other errnos.
Version
v26.7.0
Platform
Linux x64
Subsystem
fs / src/node_file.cc
What steps will reproduce the bug?
Output:
What is the expected behavior? Why is that the expected behavior?
err.codeshould be'EINVAL'anderr.errnoshould be-22. The underlyingfailure is a plain
EINVALfromrmdir(2), which POSIX requires for a path whosefinal component is
..Every other
fserror carries a usablecode, anderr.code === 'EINVAL'is thedocumented way to branch on
fsfailures, so an empty string breaks ordinary errorhandling. The message is also malformed: it begins with
,because the empty codeis interpolated into it.
What do you see instead?
errnois-4094(UV_UNKNOWN) andcodeis the empty string.Additional information
This is not specific to
EINVAL.RmSync()insrc/node_file.cctranslates thestd::error_codefromstd::filesystem::remove_all()with a hardcoded chain thatcovers four values:
operation_not_permitted->EPERMdirectory_not_empty->ENOTEMPTYnot_a_directory->ENOTDIRpermission_denied->EACCES(EPERMon Windows)Anything else falls through to the tail of the function:
So the mapping is an allowlist rather than a translation, and any errno outside
those four surfaces as
UV_UNKNOWNwith an emptycode.EINVALis simply theone that is easiest to trigger from user code.
The async form is unaffected, because
fs.rm()andfsPromises.rm()use the JSrimrafand reportEINVALcorrectly.Noting for context that the dot segment handling in #61958 is being addressed
separately at the JS layer. That change stops
a/.from reaching this code path,but it does not fix the mapping, which stays reachable for other errnos.