-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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: un-deprecate existsSync(), throw error on EPERM #4077
Conversation
fs.existSync() is actually a pretty useful convenience function, “fix” its technical shortcoming by throwing on EPERM errors so we can un-deprecate it.
I'm in favor of keeping I'm not in favor of having it throw, because at that point you could just use |
You can't though. You can't "just use accessSync()" if the code happens to be run on windows, that's awful.
I think the current behavior is actually worse. |
I'm not sure I understand what is the difference with the |
Isn't one of the biggest gripes about getting rid of |
No, it's that the others require excess options and stuff. Ideally, |
@targos hmmm, I'm not really sure? As far as I can tell you can have an EPERM both if the file does and doesn't exist, is that also the case for EACCESS? The only difference then would be I don't think you can fix an EPERM from within the node apis. |
What excessive options? |
Take this example:
For the last call, the |
Sorry, It's that for Maybe this isn't really an issue, idk. |
Adding a thow in a place that wasn't throwing before is a |
It definitely is, and not on Windows-only. There are more cases where you can't be sure if a file exists or not, the most simple of them being: > fs.statSync('/root/x')
Error: EACCES: permission denied, stat '/root/x' The above result is equivalent in case when There are also other cases when you can't be sure if the file exists or not, for example remote mounts, fuse-whatever, etc. |
To correspond with it's name, the function fs.existsSync = function(path) {
nullCheck(path); // No need to catch invalid paths it if we start throwing errors
try {
binding.stat(pathModule._makeLong(path));
return true;
} catch (e) {
if (e.code !== 'ENOENT')
throw e; // ENOENT — the file doesn't exist. Everything else — who knows?
return false;
}
}; with a corresponding But that would break existng code very badly. |
FWIW, I'm -1 on the change. If there is anything we can improve in the libuv layer (we emulate access(2) on Windows after all) in order to behave more consistently with Unix, give a shout at libuv/libuv. |
I'm generally +-0 on this change but willing to defer to @saghul and @bnoordhuis' judgement on it. |
Is anyone positive for undeprecating it and leaving it;s functionality? Should |
@Fishrock123 see the example here #4077 (comment) There is no data to say it exists or it doesn't. |
Which is why I suggested for it to throw, because neither answer is correct. On second thought, I guess leaving it alone otherwise is fine-ish. |
If existsSync is going to throw then what's the advantage over accessSync? I don't think there is a silver bullet here. Some say that are only interested in knowing if a file exists, but we can't really give an answer to that if the containing directory doesn't have the right permissions. |
Exactly. This is why I'm in favor of the status quo, of keeping it deprecated. If the main complaint is really about having to use try/catch, we could consider introducing a We could also explain the above in the documentation for why it is deprecated. |
Please don't change the API behaviour and keep the same name. Right now, I can write portable code by looking for access, and if its not there, I can use exists. If there are two versions of exists, then I'll actually need to include node-semver, and check the node version to see what its API is, or perhaps construct some more complex code. Though, frankly, I'm sometimes just using neither (edit: neither exists or access), replacing them with stat, which works on all node versions, and my CLIs usually only want rough checks for existence, such as: use the first dir that exists of A or B to store the data files. I'm OK with deprecating and then deleting APIs we don't like, or that don't match node convention. It makes node better in the long run. If we need something slightly different from access, because access is insufficiently like exists on some platforms, then I suggest proposing a new fs API to do that, with a new name. |
I'm +1 for @domenic's API concept. More explicit and predictable. There's been a lot of argument over what "exists" means, while it is much easier to define "accessible". |
Proposal
Question: Would we accept that in core? |
Does it really solve people's problems? My understanding is they want a boolean return value. |
I still don't really think that's adequate, but this can be used in a boolean check if needed, or check the error if desired. |
I think the point of the access check is, is it accessible or isn't it. don't see what an error would need to convey. |
Hmmm, thinking about it again, I don't think people care that much about accessible, but more if a file exists. I.e. Does any config exist yet, or should we make a new one? If it exists there, but isn't accessible, you can't say it doesn't exist or the program will probably break. |
Bad example. In that case you'd open the config file with Or maybe it's a great example of why |
+1 for undeprecating existsSync. I am interested in the intention, expressed via the presence of a file in the filesystem, that it should be processed - summarised as a simple boolean that doesn't require me to treat its presence or absence as "exceptional". I think we're all familiar with the recommendation not to use exceptions for normal program control flow. Whether later FS operations on the file end up throwing, as a result of a permissions/access failure, or a race condition that removed it in the intervening time, is absolutely fine with me - that condition then is exceptional. If exists/existsSync throws early if it can detect one of those exceptional conditions I really don't mind. Also whether it returns a genuine boolean or simply a truthy/falsy value I also really don't mind. fs.accessErrorSync solves a problem I don't have. I, and I think thousands of others, feel that there's absolutely nothing problematic with the definition of whether a file "exists" or not. I think we are perfectly happy with the fact that the mere existence of a file at a particular point in time provides no guarantees that anything can actually be done with it at a later point in time. There's no need to confuse/couple these two requirements together. |
@amb26 repeating myself: we can't give a straight answer to whether a file exists or not if its inside a directory which we cannot enter. Example: /root/.ssh/id_rsa (your user does not have the rights to access /root, but the file exists) What should existsSync return? It does exist, but we cannot know that. |
@saghul - I'm perfectly happy with an exception as the answer to that |
Yes, that's correct. A thrown exception defeats the purpose.
This argument has been repeatedly both stated and defeated in the referenced thread. It's an overly obsessive and fixated answer, and seems to be a base argument for the original deprecation. You are technically correct, yet these edge cases are not valid justification for throwing the baby out with the bathwater. It's "We can't be sure of the answer, therefore we'll tell you nothing, and in fact, not allow you to ask." Think about the logic you're applying to this function applied to restaurant dining. "Do you still have the fish special?" The fact that the fish special cannot be PROVEN to not exist does not matter. Refusing to say yes or no based on current available data is not MORE useful than returning boolean true / false. No scientist says that they can't reach a conclusion since it's technically true that cause and effect can never be proven, or it's possible that the entire world is a simulation in the scientist's mind, therefore they should assert nothing. It's technically TRUE, but that doesn't mean that technical trueness is always deterministic as to what information is USEFUL, or if those technically true things are really all that relevant. That said, in the world of JavaScript, were it the case that file access is the issue (if that can be determined), then it seems perfectly legitimate, as someone else suggested, to return a "falsy" value such as undefined, such that a boolean test will still tell you that the file cannot be found. And, in the event that someone needs to care about the difference between "exists" and "not found", there are other functions that CAN throw error values which can be used to split out different behavior. |
❤️ I could not agree more - this is a perfect analogy. I guess if I was going to recap my arguments from #1592 without ranting on and on, it would be just to say "it's not Node's job to fix this particular problem, either for operating system devs or application devs." The only issue the philosophical position resolves (the race condition) is rare, not generally a problem with the use-cases described at length, and present in every other language that provides the same metaphors. Document it and move on. |
@domenic wrote:
See pull request #4217 which adds an @domenic, if you really think that's a good idea, please merge it. But I think we shouldn't merge it; we should merge this PR #4077 instead. Everything wrong with #4077 is also wrong with #4217 as well. Race conditions? Check. Potential for misuse? Check. Easy to use? Check. But #4217 clutters the API with a function that's redundant with |
@dfabulich The argument for adding a function like |
@trevnorris I've read all the threads on this. I get it. That's why this PR #4077 adds the "throw error on EPERM" stuff. And that's also why I support merging it. (I agree with those who say that it would require a semver bump. To me, that sounds like a great addition to Node 6.) #4077 is our best bet. My PR #4217 is an acceptable second-best option. |
OK, there are plenty of edge cases where In our application we use I think that a large part of the disagreement comes from a difference of perspective between library developers and application developers:
You are trying to prevent library implementers from shooting themselves in the foot. Fine. But don't forget the application developers. For them backward compatibility may be more important. It is important that their code, and their dependencies, which might not be all new and shiny, don't break because of a missing API. |
@dfabulich The "throw in EPERM" is why |
@trevnorris I don't understand that position. The "other" core dev position was "just try to open the file, and try/catch that." How is try/catch a problem here but a recommended answer there? I don't understand all these protectionist positions. Bad developers will write bad code - in any language. I've found security holes in Erlang projects, Ruby projects, Scala projects, C# projects... Every one of them was suppose to be "more secure than that awful PHP." Sooner or later it's not the language's job to fix everything a human might do. @bnoordhuis in PR #4217 said basically "you can't have it because it's a TOCTOU pathway and you can't be trusted with this awesome responsibility." Are you going to stop allowing people to run Node as root, too? That's WAY more dangerous, and not discussed at all. There's a huge pile of stuff in NodeJS that throws. They're well documented and understood, and the developer's fault if they don't catch it. EPERM here is a LITTLE weird to me... but WAY less weird than deprecating existsSync in the first place. If I just want to know if the file exists, I maintain that it's less clear, less readable, and less maintainable to rely on a side effect of another function to determine that when there was a perfectly good function already that handled it. You're spending a huge amount of time here figuring out how to find alternatives for something people want to do, solely because you're deleting an already functional way that they have today. I can see what's going to happen here. There's going to be a hugely-upvoted Q&A on StackOverflow about this. The most popular answer is going to be a monkey-patch that still has all the same issues as the original. Maybe that's the best answer for now - it's certainly what I'm going to use. I check for file-existence a LOT without immediately wanting to open and read the file. (Which you still have to try/catch.) It's a valid and useful pattern to me, and I have no intention of saddling some future maintainer with an ugly comment that says "This call to accessibleSync doesn't really need access to the file. We're just seeing if it exists, but since the NodeJS devs took away existsSync we're calling this instead. So just ignore the fact that it says accessible..." |
Also, as a side note: the original call to deprecate |
We don't always agree. ;-)
My position isn't about that. It's that the API itself is fundamentally flawed. The point of
The point I'm trying to make is a file "just existing" is ambiguous. Leading to one reason why
Then what does it matter?
The documentation states:
So it achieves for all intents and purposes the same thing but is better defined. With the added bonus of being more performant. |
@trevnorris @crrobinson14 I think the better solution would be to preserve if (!fs.existSync('foo.txt')) { ... // Doesn't exist or isn't accessible - backward-compatible
if (fs.existSync('foo.txt') === false) { ... // No access error thrown, but no file found
if (fs.existSync('foo.txt') === undefined) { ... // Access error thrown when trying to determine existence
if (fs.existSync('foo.txt')) { ... // Hey, look, there's a file there! Cool. I think this is a slightly better pattern than just a true/false return for |
@trevnorris I get most of what you're saying except this part:
It sounds like you're suggesting
or
are more readable and better practice than:
No reminder to future maintainers about your intentions with the last option. This is what I'm stuck on. It just seems so awkward to me. I feel like I need to clarify my intentions to future devs in every spot I do this because they may not realize at a glance that I'm only checking existence. I do get the try/catch comment. I totally get the |
@matthew-dean Actually I'd side with the core devs on that point. My position is that using side effects as primary logic pathways is unclear. Having to I can totally see the value of |
@crrobinson14 I can respect that. I'm just trying to complain less and find something closer to an acceptable middle ground. If accessibleSync were to never throw an error, I think that would work for me. OR some other non-throwy method that returned a queryable object, like: if(fs.safeAccessSync('foo.txt').accessible) { ...
// or
if(fs.safeAccessSync('foo.txt').exists) { ...
// or
if(fs.safeAccessSync('foo.txt').writable) { ... Something that was a safe and concise utility method that could easily return existence (or accessibility, or both) without throwing errors - to me, THAT would be a perfectly fine replacement for (Although, of course, "safeAccessSync" is a horrible name.) |
@matthew-dean @trevnorris Just a suggestion, what if you added an
than relying on default behavior that does the same, or using |
I have a proposal:
People who use the old (bad) APIs will be happy: their code will continue to work. You will be happy: people who want the old APIs to stay around won't be polluting your debates. Once the new APIs are out, we'll see. If 2 years down the road a large majority of modules have switched to the new APIs, then it might be time to deprecate the old functions. If, on the other hand, the new API got poor adoption, then the old functions should stay. Let time (and the Darwinian process) sort this out. There is no reason to rush. |
@matthew-dean - yes, I agree completely. The point is that existsSync, regardless of its safety/consistency/efficiency, is a compact and easily recognised means of conveying intention. |
@crrobinson14 I thought of something like that too. if (fs.accessibleSync(filePath, fs.F_EXISTS)) { ... } And, hey, you know what? If one were being really clever, one could add a bit of sugar to the above method and write: if (fs.existsSync(filePath)) { ... } :P But, look, if it were the first option, I would be fine. |
@bjouhier This also seems reasonable. If a clearly better approach DOES manifest, then like I've said, yeah for sure, I would happily say goodbye to |
It seems the word "access" is leading to incorrect conclusions, like:
Which bring the need to explain the usage of API in context, such as:
With the
On Linux at least if a file can be viewed then it can be potentially accessed. Thus it exists. But this is not true on Windows. Hence why My only goal here is to make sure the API is well defined across platforms. |
@trevnorris I'd use it. In most scripts, I'd use it without wrapping it. |
I don't really know where to go with this, so I'm gona close it. |
This is my conclusion from #1592 (comment).
A significant amount of people find specifically
existsSync()
useful (especially in simple CLI apps) and it seems unreasonable to require a module for this. Keepexists()
deprecated as it breaks our errback API convention.This commit makes
existsSync()
throw onEPERM
. The technical problem withexistsSync()
(besides a race condition if used badly) is that you cannot know if a file does or doesn't exist under Windows with improper permissions.Throwing in that case really isn't that bad of an idea, since the regular output will probably cause errors anyways down the line, and I don't think these errors can be remedied from Node.js. Instead, error so the end user can clean up their file permissions. Catching this error in user code isn't really worthwhile due to these reasons, so suggest against it.
Discuss. cc @nodejs/collaborators