The documentation of FileManager.fileExists(at:) includes this note:
Note:
Attempting to predicate behavior based on the current state of the file system or a particular file on the file system is not recommended. Doing so can cause odd behavior or race conditions. It’s far better to attempt an operation (such as loading a file or creating a directory), check for errors, and handle those errors gracefully than it is to try to figure out ahead of time whether the operation will succeed.
But I'm not seeing a lot of promises about the structure of returned errors in API documentation. For example, when the file doesn't exist String.init(contentsOf:encoding:) currently returns NSError(domain: "NSCocoaErrorDomain", code: 260) plus a nested POSIX-domain NSError. I'm assuming that the thrown error will always be an NSError. I'm assuming that it will always be a Cocoa error wrapping the POSIX error, rather than just throwing the POSIX error directly. I'm assuming that the Cocoa code will always be 260 (NSFileReadNoSuchFileError) rather than 4 (NSFileNoSuchFileError). But generally speaking if a behavior is not documented then it's your fault if you depend on it, and the author reserves the right to change the behavior at any time.
Are the throwing behaviors of APIs like String.init(contentsOf:encoding:) documented anywhere? If not, is it because the authors want to reserve the ability to change the behavior? And if there will be no change in behavior, can we just document these things once and be done with it?
The long answer: String.init(contentsOf:encoding:), String.init(contentsOfFile:encoding:), and many similar initializers on String are extensions defined in Foundation, and were introduced early on to bridge the gap between existing NSString APIs and the String type to bring existing functionality to new Swift types. (There are likewise extensions on other types which historically have done something similar.)
Although now largely implemented in pure Swift, the original implementations were effectively:
// Not literally:
extension StdlibType {
init(sameArgsAsNSType) {
NSType(sameArgsAsNSType) as StdlibType
}
func sameMethodAsNSType() {
(self as NSType).sameMethodAsNSType()
}
}
This means that String.init(contentsOf:encoding:) historically threw whatever NSString.init(contentsOfURL:encoding:) threw because it just called down to that, and the errors thrown by NSString are not explicitly documented, at least partially because they are OS-specific and can change over time, with, e.g., the introduction of new OS-level/framework-level features.
It's certainly possible to audit all such extensions and explicitly declare what errors they can throw (and possibly lock those down), but I'm not sure I personally see much benefit.
(CocoaError in general is somewhat best treated as any Error — largely opaque and not really intended for ergonomic inspection; I can't recall off the top of my head ever seeing error codes explicitly being documented on APIs.)
I would love to never have to Inspect any Error values, and you’ve confirmed my suspicions about doing so being unadvisable. My real question then is:
Is the FileManager documentation correct that checking fileExists(at:) is also an anti-pattern for its own reasons, and if so, is it really the case that there is no safe way to read a file and create it if it doesn’t exist yet? It sounds absurd, so I feel I must be misinterpreting something…
It is correct in the general case, yes, but with nuance. There is an inherent race condition in checking "does this file exist and if not, create it" because theoretically, another process can come in and create the file in between your check and attempted creation.* So you check for the file, see that it doesn't exist, attempt to create it, and fail with "file already exists" — in that case, the check didn't actually get you anything, because you would've gotten the same result if you'd just tried to create the file blindly.
In practice, this may or may not matter to you. If you're working within a sandboxed environment where other processes can't come in and create files, then this is much less likely to be an issue. And if you're using the existence of the file to signify some state beyond just avoiding overwriting data, then checking for the existence of the file may be necessary for other reasons.