runtime: File open should have non-throwing alternatives
Opening a file today in. NET today requires developers to introduce exception handling into their code. That is often unwanted and adds unnecessary complexity to otherwise straight forward code. .NET should offer non-throwing alternatives to facilitate these cases.
Rationale and Usage
File operations in .NET today require developers to introduce exception handling for even the simplest of operations. This is due to a combination of the operations being inherently unpredictable in nature and the BCL only provides exceptions as a way to indicate failure.
This is problematic because it forces exception handling into every .NET app which use the file system. In many cases developers would prefer to use simple if
checks as it fits into the flow of their code. This is particularly true of lower level code which tends to be written with local control flow vs. exceptions. Using .NET though there is no way to avoid exceptions when dealing with the file system.
Additionally this is particularly annoying when debugging code in Visual Studio. Even when code has proper exception handling VS will still break File.Open
calls when first chance exceptions are enabled (quite common). Disabling notifications for these exceptions is often not an option because in addition to hiding benign case it could hide the real error that you’re trying to debug.
Many developers attempt to work around this by using the following anti-pattern:
static bool TryOpen(string filePath, out FileStream fileStream) {
if (!File.Exists(filePath)) {
fileStream = null;
return false;
}
fileStream = File.Open(filePath);
return true;
}
This code reads great but is quite broken in at least the following ways:
- The developer mistakenly assumes that
File.Exists
is a side effect free operation. This is not always the case. Example is whenfilePath
points to a named pipe. CallingExists
for a named pipe which is waiting for a connection will actually complete the connection under the hood (and then immediately break the pipe). - The
File.Open
call can still throw exceptions hence even with theFile.Exists
predicate. The file could be deleted between the call, it could be open as part of an NTFS transactional operation, permissions could change, etc … Hence correct code must still use exception handling here.
A more correct work around looks like the following. Even this sample I’m sure is subtle to cases that would have an observable difference from just calling File.Open
):
static bool TryOpen(string filePath, out FileStream fileStream) {
if (!filePath.StartsWith(@"\\.\pipe", StringComparison.OrdinalIgnoreCase) && !File.Exists(filePath)) {
fileStream = null;
return false;
}
try {
fileStream = File.Open(filePath);
return true;
}
catch (Exception) {
fileStream = null;
return false;
}
}
To support these scenarios .NET should offer a set of file system helpers that use return values to indicate failure via the Try
pattern: File.TryOpen
.
Proposed API
public static class File {
public static bool TryOpen(
string path,
FileMode mode,
FileAccess access,
FileShare share,
out FileStream fileStream);
public static bool TryOpen(
string path,
FileMode mode,
out FileStream fileStream);
public static bool TryOpen(
string path,
FileMode mode,
FileAccess access,
out FileStream fileStream);
Details
- Considered
TryOpenExisting
instead ofTryOpen
as there is prior art here in Mutex.TryOpenExisting. That seems like a good fit forMutex
as it had anOpenExisting
method. Not a good fit forFile
which hasOpen
and a series of other modes viaFileMode
that don’t like up with the terminology Existing. - Considered expanding this to other types like
Directory
. Similar rationale exists for that but theFile
examples are much more predominant. Can expand based on feedback. - Any pattern which involves
File.Exists
is inherently broken. When many developers are using it for common patterns it generally indicates there is a more correct API that has not been provided.
About this issue
- Original URL
- State: open
- Created 6 years ago
- Reactions: 100
- Comments: 36 (28 by maintainers)
I love the proposal, but would:
Are you feeling
TryOpenAsync
with that pattern?Cloudflare: How we scaled nginx and saved the world 54 years every day
Once you eliminate exceptions there is really no way to have it work as easily with
using
. There is always anif
check that has to happen. Consider both cases a)return null
and b)return bool
).…how does this interact with the fact that
Read
/Write
(and all their variants) can still throw I/O errors? A handle having been valid at one point won’t guarantee it stays that way, right?@migueldeicaza
While I like discriminated unions, the problem I see with
Result<T, E>
is that we already have the equivalent error feature.You know, exceptions.
Essentially we’d be propagating a second, “softer” form of exceptions, ones declared as part of the API. Given we already have exceptions, I think I’d prefer going the Midori route: methods either explicitly throw a specific set of exceptions that can be recovered from, or none. Things that can’t be recovered from - which is usually from programmer error - cause abandonment (essentially, exit the running task/process/application).
The language is already starting to move in the direction where such a thing might be viable; we’re getting non/nullable reference types, meaning
NullReferenceException
(which is - usually - caused by programmer error, should be fixed, and would cause abandonment under this model) should go away. You can write code to takeSpan<T>
s orMemory<T>
s for slices, which help avoid index-out-of-bounds… as would (hopefully) the upcomingRange
feature.So, what’s the progress on this after 3 years?
If the developer wants to handle error conditions, she should use the normal non-Try API, right? This is the way error conditions are propagated in .NET: exceptions. Otherwise, I don’t get it.
One possible consideration is to wait until C# gets discriminated unions, and then change our APIs to support the
Result<T,E>
idiom as seen in languages like Rust, F# and Swift:SocketError.Success
andWebSocketError.Success
is existing prior art for having success code in error code enums.Wouldn’t that defeat the purpose of the
Try
pattern? I thinkTry
only works when there’s one response to error.Per discussion in dotnet/runtime#926 this is not possible to do in a reasonable cross platform way, for many common cases.
@jaredpar Do you think you’d consider introducing an error enum for this API instead of just returning bool so that it can be used for scenarios like the one outlined in dotnet/runtime#926?
where it’s needed to treat very specific failures specially and completely ignore them because they are expected (as opposed to other errors).
I like the idea at face value with the given history of the
Try
pattern. But, thinking through some scenarios, I’m no longer sure.I think part of the problem is the ambiguity of
File.Open
. This ambiguity leads you to the need for something likeTryOpen
; but also telescopes the ambiguity, in my mind. I’d first try to address the ambiguity because there’s multiple reasons why you’d callFile.Open
and thus multiple things you’d want to do when unsuccessful. This leads to questions about error codes, etc. Off the top of my head, I’d consider deprecatingOpen
and focus onOpenRead
,OpenWrite
, andOpenText
. But, withTry
variants of those, their usage becomes more awkward in light ofIDisposable
.It can be made to work with
using
by switching what goes in theout
parameter:@migueldeicaza i like the idea of
Status
(for clever transient strategies) but i would put as non mandatory overload, something like:That’s true, although I’m worried the
using
will be easier to forget if you returnbool
. Maybe that’s just me though.Why not just return
null
on failure, to make it easy to combine withusing
?I know it doesn’t fit well with the
bool TryXxx(..., out var result)
convention, but I think the ease of use withusing
outweighs this.When nullable types make it into the language, turn that into a
FileStream?
.This should work
I think this is a good idea. But what about combining this with
using
?Since a stream is disposable, I think a new pattern for opening files should work well with using statements. Do you have any ideas for that? Something like the below won’t compile. (Using variables are immutable so they cannot be passed by ref).
(Or am I overlooking something?)