-
Notifications
You must be signed in to change notification settings - Fork 30.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fs: implement fsp.exists
#39968
base: main
Are you sure you want to change the base?
fs: implement fsp.exists
#39968
Conversation
lib/internal/fs/promises.js
Outdated
@@ -808,11 +808,22 @@ async function readFile(path, options) { | |||
return handleFdClose(readFileHandle(fd, options), fd.close); | |||
} | |||
|
|||
async function exists(path) { | |||
validateString(path, 'path'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think access()
has already validated the path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but access()
is called in the try
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the problem is, if we got a wrong path, should exists()
return false
or throw an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lines 291 to 295 in 73d5f8a
try { | |
path = getValidatedPath(path); | |
} catch { | |
return false; | |
} |
fs.existsSync()
returns false
if path validation failed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to
Lines 279 to 284 in 73d5f8a
// fs.existsSync never throws, it only returns true or false. | |
// Since fs.existsSync never throws, users have established | |
// the expectation that passing invalid arguments to it, even like | |
// fs.existsSync(), would only get a false in return, so we cannot signal | |
// validation errors to users properly out of compatibility concerns. | |
// TODO(joyeecheung): deprecate the never-throw-on-invalid-arguments behavior |
fsPromises.exists
.Maybe we should even go further and also throw if the error is not ENOENT. For example a permission error now ends up in returning
false
but we actually don't know if the file exists or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, fs.existsSync(path)
returns false
when path validation failed, even path is not a string
. e.g.
> const fs = require('fs')
undefined
> fs.existsSync({})
false
I would think it is a bug(?), since docs says it only allows path of type <string> | <Buffer> | <URL>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to
Lines 279 to 284 in 73d5f8a
// fs.existsSync never throws, it only returns true or false. // Since fs.existsSync never throws, users have established // the expectation that passing invalid arguments to it, even like // fs.existsSync(), would only get a false in return, so we cannot signal // validation errors to users properly out of compatibility concerns. // TODO(joyeecheung): deprecate the never-throw-on-invalid-arguments behavior , I think we should not make the same mistake with
fsPromises.exists
.
Maybe we should even go further and also throw if the error is not ENOENT. For example a permission error now ends up in returningfalse
but we actually don't know if the file exists or not.
If we throws, the behaivor of 3 APIs are very different:
fs.exists()
: wrong callback signature;fs.existsSync()
: eats errors;fsPromise.exists()
: throws errors.
And actually they just do the same thing in different async ways. I think we shouldn't make the behaivor more chaotic.
I think there're 2 ways to choose:
fsPromise.exists()
: never throws;- Delete
fs.existsSync()
's TODO and make it throws.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to make fs.existsSync
throw, we should probably make the error swallowing go through a complete deprecation first to make sure this won't break the ecosystem.
* Returns: {Promise} Resolved as `true` if the given path exists, otherwise, | ||
resolved as `false`. | ||
|
||
Test whether or not the given path exists by checking with the file system. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't really check if the file exists, which is why the name is misleading, see #39960 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. It makes sense. Do you have a better name suggestion? Or like os.path.exists
on Python, is a sepecial note sufficient?
Leaving this out in the fs.promises impl was intentional given that it's a fairly significant anti-pattern, especially in an async api like fs.promises. The check is never going to be 100% reliable because the path could be created/deleted in between the time it is checked and the time the microtask continuation happens. |
While this is true, how is this different than something like The main selling point over |
My point about error checking is to choose one way of:
Any of the two ways is OK. But they shouldn't be different (e.g. |
It might be difficult to change the behavior of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking this as changes requested to make sure it doesn't land by accident. This PR should not land until:
- a consensus is reached regarding the need for this API to exist at all given the anti-patterns it might encourage.
- a consensus is reached regarding error handling (throw when FS permissions doesn't let it know if a file exist? Mimic
existsSync
behavior?).
I also think the documentation could use some "Instead of this, do this" code examples to illustrate which anti-patterns users should avoid.
The last comment on this PR was about 1 year ago and given that this is a useful feature to have in Node.js core, I'm asking if discussions happened or if a consensus was reached? Please reconsider this feature to be included in Node.js core, I often end up copying/pasting this code: import fs from 'node:fs'
export const isExistingPath = async (path: string): Promise<boolean> => {
try {
await fs.promises.access(path, fs.constants.F_OK)
return true
} catch {
return false
}
} Whereas, I could be using: import fs from 'node:fs'
await fs.promises.exists(path) IMO about the consensus:
|
if (err.code === 'ENOENT') { | ||
return false; | ||
} | ||
// We are unable to determine if the file exists or not, | ||
// like encountering a permission error. | ||
throw err; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should always (including the fact that this is a permission error) return false
in this catch
block, and should never throw an error to mimic existsSync
API.
@divlo Since you claim to use this quite often and because you appear to prefer the variant that does not distinguish between
|
@tniessen In my use cases specifically, I could replace The only reason, I suggest not distinguishing between errors, is to have the same behavior. Note: You missed the ping for my name (typo). |
@tniessen I'm 90% certain that basically every use case I have meets those criteria. Literally anytime I'm checking to see if something exists, it's just to ensure I'm not overwriting a file that I've already written or to follow up with a read, which AFAIK also doesn't necessarily require any of those situations to be the case (I can handle checking if the file is readable separately). |
1 similar comment
@tniessen I'm 90% certain that basically every use case I have meets those criteria. Literally anytime I'm checking to see if something exists, it's just to ensure I'm not overwriting a file that I've already written or to follow up with a read, which AFAIK also doesn't necessarily require any of those situations to be the case (I can handle checking if the file is readable separately). |
Personally, I am not convinced that the existing behavior is desirable. The existing function is even deprecated.
As far as I can tell, based on the discussion here, the main questions are:
Maybe the answer to all of these is "no", in which case adding the
I have a feeling that these are the race conditions that were mentioned earlier. When an application is about to write a file and does not want to overwrite an existing file, it's often a good idea to let the OS handle it by specifying But I do realize that such explicit checks might be needed if a significant amount of work needs to be done between checking the existence of the file and creating it.
Similarly, in many cases, it probably makes sense to just read the file and to handle Again, I do acknowledge that there are use cases where an existence check really is the best approach, even at the risk of introducing race conditions etc. Those use cases can already be handled with a few lines of code, and the question remains: do these use cases justify adding a utility function to node that might be misleading or even incorrect and that might promote anti-patterns, including race conditions, like the existing |
My genuine and honest response to all discussion about the general topic is that the DX of having to go to access() and stat() is sub-par and exists() should do what it's name implies (setting aside whatever it currently does). I don't really care what this looks like. I honestly just want... better DX around this. It's a relatively common thing to do and it's unnecessarily complicated as-is. If that means we semver major replace exists() functionality with the best practice for doing what you can do with access(), stat(), or directly opening the file system entry, I'm fine with that. If that means doing all that (or something else) under a new method that accurately describes the function in a similar way to exists() with a strong API, I'm fine with that too. |
I'm still -1 on this for all the reasons already expressed. I don't see having this available is any improvement over what can already be done |
Quick question, given that example: import fs from 'node:fs'
const files = [] // array of strings, containing the path of different files
await Promise.all(
files.map(async (file) => {
if (await fs.promises.exists(file)) {
// do something
}
})
) Is the promise version (with Am I making wrong assertion? |
It's difficult to say since it depends on various properties related to the thread pool, but it is likely to perform better.
If you want the existing buggy behavior of const exists = util.promisify(fs.exists);
// or
const exists = (p) => new Promise((r) => fs.exists(p, r)); It's also not difficult to arrive at a solution that handles errors properly. For example: const exists = (p) =>
fs.promises.access(p).then(
() => true,
(e) => (e.code === 'ENOENT' ? false : Promise.reject(e))
); |
Fixes: #39960
cc @bnb