Handling I/O errors in .NET
In addition to the exceptions that can be thrown in any method call (such as an OutOfMemoryException when a system is stressed or an NullReferenceException due to programmer error), .NET file system methods can throw the following exceptions:
- System.IO.IOException, the base class of all System.IO exception types. It is thrown for errors whose return codes from the operating system don't directly map to any other exception type.
- System.IO.FileNotFoundException.
- System.IO.DirectoryNotFoundException.
- DriveNotFoundException.
- System.IO.PathTooLongException.
- System.OperationCanceledException.
- System.UnauthorizedAccessException.
- System.ArgumentException, which is thrown for invalid path characters on .NET Framework and on .NET Core 2.0 and previous versions.
- System.NotSupportedException, which is thrown for invalid colons in .NET Framework.
- System.Security.SecurityException, which is thrown for applications running in limited trust that lack the necessary permissions on .NET Framework only. (Full trust is the default on .NET Framework.)
Mapping error codes to exceptions
Because the file system is an operating system resource, I/O methods in both .NET Core and .NET Framework wrap calls to the underlying operating system. When an I/O error occurs in code executed by the operating system, the operating system returns error information to the .NET I/O method. The method then translates the error information, typically in the form of an error code, into a .NET exception type. In most cases, it does this by directly translating the error code into its corresponding exception type; it does not perform any special mapping of the error based on the context of the method call.
For example, on the Windows operating system, a method call that returns an error code of ERROR_FILE_NOT_FOUND
(or 0x02) maps to a FileNotFoundException, and an error code of ERROR_PATH_NOT_FOUND
(or 0x03) maps to a DirectoryNotFoundException.
However, the precise conditions under which the operating system returns particular error codes is often undocumented or poorly documented. As a result, unexpected exceptions can occur. For example, because you are working with a directory rather than a file, you would expect that providing an invalid directory path to the DirectoryInfo constructor throws a DirectoryNotFoundException. However, it may also throw a FileNotFoundException.
Exception handling in I/O operations
Because of this reliance on the operating system, identical exception conditions (such as the directory not found error in our example) can result in an I/O method throwing any one of the entire class of I/O exceptions. This means that, when calling I/O APIs, your code should be prepared to handle most or all of these exceptions, as shown in the following table:
Exception type | .NET Core/.NET 5+ | .NET Framework |
---|---|---|
IOException | Yes | Yes |
FileNotFoundException | Yes | Yes |
DirectoryNotFoundException | Yes | Yes |
DriveNotFoundException | Yes | Yes |
PathTooLongException | Yes | Yes |
OperationCanceledException | Yes | Yes |
UnauthorizedAccessException | Yes | Yes |
ArgumentException | .NET Core 2.0 and earlier | Yes |
NotSupportedException | No | Yes |
SecurityException | No | Limited trust only |
Handling IOException
As the base class for exceptions in the System.IO namespace, IOException is also thrown for any error code that does not map to a predefined exception type. This means that it can be thrown by any I/O operation.
Important
Because IOException is the base class of the other exception types in the System.IO namespace, you should handle in a catch
block after you've handled the other I/O-related exceptions.
In addition, starting with .NET Core 2.1, validation checks for path correctness (for example, to ensure that invalid characters are not present in a path) have been removed, and the runtime throws an exception mapped from an operating system error code rather than from its own validation code. The most likely exception to be thrown in this case is an IOException, although any other exception type could also be thrown.
Note that, in your exception handling code, you should always handle the IOException last. Otherwise, because it is the base class of all other IO exceptions, the catch blocks of derived classes will not be evaluated.
In the case of an IOException, you can get additional error information from the IOException.HResult property. To convert the HResult value to a Win32 error code, you strip out the upper 16 bits of the 32-bit value. The following table lists error codes that may be wrapped in an IOException.
HResult | Constant | Description |
---|---|---|
ERROR_SHARING_VIOLATION | 32 | The file name is missing, or the file or directory is in use. |
ERROR_FILE_EXISTS | 80 | The file already exists. |
ERROR_INVALID_PARAMETER | 87 | An argument supplied to the method is invalid. |
ERROR_ALREADY_EXISTS | 183 | The file or directory already exists. |
You can handle these using a When
clause in a catch statement, as the following example shows.
using System;
using System.IO;
using System.Text;
class Program
{
static void Main()
{
var sw = OpenStream(@".\textfile.txt");
if (sw is null)
return;
sw.WriteLine("This is the first line.");
sw.WriteLine("This is the second line.");
sw.Close();
}
static StreamWriter? OpenStream(string path)
{
if (path is null)
{
Console.WriteLine("You did not supply a file path.");
return null;
}
try
{
var fs = new FileStream(path, FileMode.CreateNew);
return new StreamWriter(fs);
}
catch (FileNotFoundException)
{
Console.WriteLine("The file or directory cannot be found.");
}
catch (DirectoryNotFoundException)
{
Console.WriteLine("The file or directory cannot be found.");
}
catch (DriveNotFoundException)
{
Console.WriteLine("The drive specified in 'path' is invalid.");
}
catch (PathTooLongException)
{
Console.WriteLine("'path' exceeds the maximum supported path length.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("You do not have permission to create this file.");
}
catch (IOException e) when ((e.HResult & 0x0000FFFF) == 32)
{
Console.WriteLine("There is a sharing violation.");
}
catch (IOException e) when ((e.HResult & 0x0000FFFF) == 80)
{
Console.WriteLine("The file already exists.");
}
catch (IOException e)
{
Console.WriteLine($"An exception occurred:\nError code: " +
$"{e.HResult & 0x0000FFFF}\nMessage: {e.Message}");
}
return null;
}
}
Imports System.IO
Module Program
Sub Main(args As String())
Dim sw = OpenStream(".\textfile.txt")
If sw Is Nothing Then Return
sw.WriteLine("This is the first line.")
sw.WriteLine("This is the second line.")
sw.Close()
End Sub
Function OpenStream(path As String) As StreamWriter
If path Is Nothing Then
Console.WriteLine("You did not supply a file path.")
Return Nothing
End If
Try
Dim fs As New FileStream(path, FileMode.CreateNew)
Return New StreamWriter(fs)
Catch e As FileNotFoundException
Console.WriteLine("The file or directory cannot be found.")
Catch e As DirectoryNotFoundException
Console.WriteLine("The file or directory cannot be found.")
Catch e As DriveNotFoundException
Console.WriteLine("The drive specified in 'path' is invalid.")
Catch e As PathTooLongException
Console.WriteLine("'path' exceeds the maximum supported path length.")
Catch e As UnauthorizedAccessException
Console.WriteLine("You do not have permission to create this file.")
Catch e As IOException When (e.HResult And &h0000FFFF) = 32
Console.WriteLine("There is a sharing violation.")
Catch e As IOException When (e.HResult And &h0000FFFF) = 80
Console.WriteLine("The file already exists.")
Catch e As IOException
Console.WriteLine($"An exception occurred:{vbCrLf}Error code: " +
$"{e.HResult And &h0000FFFF}{vbCrLf}Message: {e.Message}")
End Try
Return Nothing
End Function
End Module