fs: add throwIfNoEntry option for fs.stat and fs.promises.stat

Fixes: https://github.com/nodejs/node/issues/61116
Signed-off-by: Juan José Arboleda <soyjuanarbol@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/61178
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Juan José authored and GitHub committed 2026-02-11 15:02:02 +00:00
1 parent cff8bfd865
commit 31bf4fb6e5
6 files changed
+64 -8

No files matched your search

+6
View File
@@ -1719,6 +1719,9 @@ changes:
* `options` {Object}
* `bigint` {boolean} Whether the numeric values in the returned
{fs.Stats} object should be `bigint`. **Default:** `false`.
* `throwIfNoEntry` {boolean} Whether an exception will be thrown
if no file system entry exists, rather than returning `undefined`.
**Default:** `true`.
* Returns: {Promise} Fulfills with the {fs.Stats} object for the
given `path`.
@@ -4436,6 +4439,9 @@ changes:
* `options` {Object}
* `bigint` {boolean} Whether the numeric values in the returned
{fs.Stats} object should be `bigint`. **Default:** `false`.
* `throwIfNoEntry` {boolean} Whether an exception will be thrown
if no file system entry exists, rather than returning `undefined`.
**Default:** `true`.
* `callback` {Function}
* `err` {Error}
* `stats` {fs.Stats}
+3 -2
View File
@@ -188,6 +188,7 @@ function makeStatsCallback(cb) {
return (err, stats) => {
if (err) return cb(err);
if (stats === undefined && err === null) return cb(null, undefined);
cb(err, getStatsFromBinding(stats));
};
}
@@ -1621,7 +1622,7 @@ function lstat(path, options = { bigint: false }, callback) {
* ) => any} callback
* @returns {void}
*/
function stat(path, options = { bigint: false }, callback) {
function stat(path, options = { bigint: false, throwIfNoEntry: true }, callback) {
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
@@ -1630,7 +1631,7 @@ function stat(path, options = { bigint: false }, callback) {
const req = new FSReqCallback(options.bigint);
req.oncomplete = callback;
binding.stat(getValidatedPath(path), options.bigint, req);
binding.stat(getValidatedPath(path), options.bigint, req, options.throwIfNoEntry);
}
function statfs(path, options = { bigint: false }, callback) {
+6 -2
View File
@@ -1026,12 +1026,16 @@ async function lstat(path, options = { bigint: false }) {
return getStatsFromBinding(result);
}
async function stat(path, options = { bigint: false }) {
async function stat(path, options = { bigint: false, throwIfNoEntry: true }) {
const result = await PromisePrototypeThen(
binding.stat(getValidatedPath(path), options.bigint, kUsePromises),
binding.stat(getValidatedPath(path), options.bigint, kUsePromises, options.throwIfNoEntry),
undefined,
handleErrorFromBinding,
);
// Binding will resolve undefined if UV_ENOENT or UV_ENOTDIR and throwIfNoEntry is false
if (!options.throwIfNoEntry && result === undefined) return undefined;
return getStatsFromBinding(result);
}
+38 -3
View File
@@ -810,6 +810,22 @@ void AfterStat(uv_fs_t* req) {
}
}
void AfterStatNoThrowIfNoEntry(uv_fs_t* req) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
FSReqAfterScope after(req_wrap, req);
FS_ASYNC_TRACE_END1(
req->fs_type, req_wrap, "result", static_cast<int>(req->result))
if (req->result == UV_ENOENT || req->result == UV_ENOTDIR) {
req_wrap->Resolve(Undefined(req_wrap->env()->isolate()));
return;
}
if (after.Proceed()) {
req_wrap->ResolveStat(&req->statbuf);
}
}
void AfterStatFs(uv_fs_t* req) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
FSReqAfterScope after(req_wrap, req);
@@ -1105,7 +1121,9 @@ static void Stat(const FunctionCallbackInfo<Value>& args) {
ToNamespacedPath(env, &path);
bool use_bigint = args[1]->IsTrue();
if (!args[2]->IsUndefined()) { // stat(path, use_bigint, req)
if (!args[2]->IsUndefined()) { // stat(path, use_bigint, req,
// do_not_throw_if_no_entry)
bool do_not_throw_if_no_entry = args[3]->IsFalse();
FSReqBase* req_wrap_async = GetReqWrap(args, 2, use_bigint);
CHECK_NOT_NULL(req_wrap_async);
ASYNC_THROW_IF_INSUFFICIENT_PERMISSIONS(
@@ -1115,8 +1133,25 @@ static void Stat(const FunctionCallbackInfo<Value>& args) {
path.ToStringView());
FS_ASYNC_TRACE_BEGIN1(
UV_FS_STAT, req_wrap_async, "path", TRACE_STR_COPY(*path))
AsyncCall(env, req_wrap_async, args, "stat", UTF8, AfterStat,
uv_fs_stat, *path);
if (do_not_throw_if_no_entry) {
AsyncCall(env,
req_wrap_async,
args,
"stat",
UTF8,
AfterStatNoThrowIfNoEntry,
uv_fs_stat,
*path);
} else {
AsyncCall(env,
req_wrap_async,
args,
"stat",
UTF8,
AfterStat,
uv_fs_stat,
*path);
}
} else { // stat(path, use_bigint, undefined, do_not_throw_if_no_entry)
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
+6
View File
@@ -152,6 +152,12 @@ async function executeOnHandle(dest, func) {
}));
}
// File stats throwIfNoEntry: false
{
const stats = await stat('meow.js', { throwIfNoEntry: false });
assert.strictEqual(stats, undefined);
}
// File system stats
{
const statFs = await statfs(dest);
+5 -1
View File
@@ -224,5 +224,9 @@ fs.lstat(__filename, undefined, common.mustCall());
{
// Test that the throwIfNoEntry option works and returns undefined
assert.ok(!(fs.statSync('./wont_exists', { throwIfNoEntry: false })));
const opts = { throwIfNoEntry: false };
assert.ok(!(fs.statSync('./wont_exists', opts)));
fs.stat('./wont_exists', opts, common.mustSucceed((err, stats) => {
assert.strictEqual(stats, undefined);
}));
}