System.IO.IOException

An IOException is thrown when an input or output operation fails. It is the base class of FileNotFoundException, DirectoryNotFoundException and PathTooLongException, and it is also thrown directly. The most common message is The process cannot access the file because it is being used by another process.

It often points to a problem that is temporary, such as a locked file or a network share that is slow, so handling it with a retry can be reasonable.

Minimum version: >= 1.1 >= Core 1.0

Statistics

7
elmah.io logo 22

Common causes

The operation failed after the path was found. These are the usual causes.

A file that another stream or process has open

Windows locks a file while it is open, and the share mode decides what others may do with it. Opening it with FileShare.None, which is the default for many writes, blocks every other open.

var path = Path.GetTempFileName();
using var writer = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None);

var text = File.ReadAllText(path);   // IOException: the file is being used by another process

Open the file with a share mode that lets the others in. The writer allows readers, and the reader allows the writer:

var path = Path.GetTempFileName();
using var writer = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.Read);
using var reader = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

A file stays open until the stream is disposed, so your own code can lock a file against itself. A delete or move right after fails.

var path = Path.GetTempFileName();
var stream = File.OpenRead(path);   // never disposed, so the file stays locked
File.Delete(path);                  // IOException

Dispose the stream with using, so the file is released before the delete:

var path = Path.GetTempFileName();

using (var stream = File.OpenRead(path))
{
    // read from the stream
}

File.Delete(path);

An IOException is also thrown when the disk is full, when a network share disconnects, or when the path is not valid. These messages are different from the file in use message, for example There is not enough space on the disk.

Read the message, because it says which one it is. Check the free space and the connection, and handle the exception where the operation is retried or reported.

How to fix it and prevent it

Always dispose streams

Use using for every stream, reader and writer. It releases the file even when an exception is thrown in between.

using var reader = new StreamReader(Path.GetTempFileName());
var line = reader.ReadLine();

When several processes use the same file, such as a log file that another tool reads, set FileShare to say what is allowed. The default is often more restrictive than you need.

using var stream = new FileStream(Path.GetTempFileName(), FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

A lock from a virus scanner, a backup or another process is often gone after a short wait. Retry a few times with a small delay, and give up with the real exception.

var source = Path.GetTempFileName();
var target = Path.GetTempFileName();

for (var attempt = 1; attempt <= 3; attempt++)
{
    try
    {
        File.Copy(source, target, overwrite: true);
        break;
    }
    catch (IOException) when (attempt < 3)
    {
        await Task.Delay(200 * attempt);
    }
}

How to find what holds the file

The message names the file, and the stack trace shows which call tried to use it:

System.IO.IOException: The process cannot access the file 'C:\Temp\tmp52kcx1.tmp' because it is being used by another process.
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Shop.Services.LogArchiver.Archive(String path) in C:\src\Shop\Services\LogArchiver.cs:line 11
   at Program.<Main>$(String[] args) in C:\src\Shop\Program.cs:line 6

The call that failed is line 11 of LogArchiver.cs, and the message says that another process, or another stream in your own process, has the file open. Check your own code first for streams that are not disposed. For other processes, Process Explorer or Resource Monitor on Windows show which one holds the handle.

Should you catch it?

Yes, around file and network operations. Many causes are temporary, so a retry for a few attempts is reasonable. Do not retry forever, and log the exception, so you can see when it happens.

Catch the specific exceptions you can handle, such as FileNotFoundException, before IOException.

Find IOException before your users do

elmah.io logs every unhandled exception in your .NET application with its stack trace and request details, groups identical errors and notifies you when a new one appears.

Start free trial
Free 21-day trial No credit card required

Related exceptions

Frequently asked questions

How do I find which process has a file open?

On Windows, use Process Explorer to search for a handle, or Resource Monitor. On Linux, use lsof. Also check your own code for streams that are not disposed.

An IOException means the operation failed, for example because the file is locked. An UnauthorizedAccessException means the operating system refused access because of permissions.

Yes, if both sides allow it. The writer must open the file with FileShare.Read or FileShare.ReadWrite, and the reader with FileShare.ReadWrite.

For locks and network hiccups, yes. Limit the number of attempts, wait a little longer each time, and rethrow after the last one.

Let your AI agent track it down

Connect Claude Code, Cursor, VS Code or Visual Studio to the elmah.io MCP server and ask your agent to look into IOException for you. For example:

AI chat
Find the most frequent IOException in my production log and show me the line that throws it.

The agent reads the stack trace and request details from elmah.io, finds the code in your repository and proposes a fix. In Claude Code, add the server with one command:

claude mcp add --transport http --client-id claudecode elmahio https://mcp.elmah.io/mcp

The MCP server is included on every plan and is currently in beta. Set up the MCP server.

Further reading

YouTube videos

Answers from Stack Overflow

From How to tell if path is file or directory:

// get the file attributes for file or directory
FileAttributes attr = File.GetAttributes(@"c:\Temp");

//detect whether its a directory or file
if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
    MessageBox.Show("Its a directory");
else
    MessageBox.Show("Its a file");

Update for .NET 4.0+

Per the comments below, if you are on .NET 4.0 or later (and maximum performance is not critical) you can write the code in a cleaner way:

// get the file attributes for file or directory
FileAttributes attr = File.GetAttributes(@"c:\Temp");

if (attr.HasFlag(FileAttributes.Directory))
    MessageBox.Show("Its a directory");
else
    MessageBox.Show("Its a file");

By Quinn Wilson. Read the original answer on Stack Overflow.

How about using this?

if(File.Exists(data.path))
{
    // is file
}
else if(Directory.Exists(data.path))
{
   // is Folder 
}
else
{
   // invalid path
}

File.Exists() will return false if it's not a file even if the directory does exist, so if it returns true, we know we got a file, if it returns false, we either have a directory or an invalid path so next we test if it's a valid directory with Directory.Exists() if that returns true, we have a directory if not it's an invalid path.

By llamaoo7. Read the original answer on Stack Overflow.

When I faced with a similar problem, I finished with the following code:

public class FileManager
{
    private string _fileName;

    private int _numberOfTries;

    private int _timeIntervalBetweenTries;

    private FileStream GetStream(FileAccess fileAccess)
    {
        var tries = 0;
        while (true)
        {
            try
            {
                return File.Open(_fileName, FileMode.Open, fileAccess, Fileshare.None); 
            }
            catch (IOException e)
            {
                if (!IsFileLocked(e))
                    throw;
                if (++tries > _numberOfTries)
                    throw new MyCustomException("The file is locked too long: " + e.Message, e);
                Thread.Sleep(_timeIntervalBetweenTries);
            }
        }
    }

    private static bool IsFileLocked(IOException exception)
    {
        int errorCode = Marshal.GetHRForException(exception) & ((1 << 16) - 1);
        return errorCode == 32 || errorCode == 33;
    }

    // other code

}

By DixonD. Read the original answer on Stack Overflow.

The other answers rely on old information. This one provides a better solution.

Long ago it was impossible to reliably get the list of processes locking a file because Windows simply did not track that information. To support the Restart Manager API, that information is now tracked. The Restart Manager API is available beginning with Windows Vista and Windows Server 2008 (Restart Manager: Run-time Requirements).

I put together code that takes the path of a file and returns a List<Process> of all processes that are locking that file.

static public class FileUtil
{
    [StructLayout(LayoutKind.Sequential)]
    struct RM_UNIQUE_PROCESS
    {
        public int dwProcessId;
        public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
    }

    const int RmRebootReasonNone = 0;
    const int CCH_RM_MAX_APP_NAME = 255;
    const int CCH_RM_MAX_SVC_NAME = 63;

    enum RM_APP_TYPE
    {
        RmUnknownApp = 0,
        RmMainWindow = 1,
        RmOtherWindow = 2,
        RmService = 3,
        RmExplorer = 4,
        RmConsole = 5,
        RmCritical = 1000
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    struct RM_PROCESS_INFO
    {
        public RM_UNIQUE_PROCESS Process;

        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
        public string strAppName;

        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
        public string strServiceShortName;

        public RM_APP_TYPE ApplicationType;
        public uint AppStatus;
        public uint TSSessionId;
        [MarshalAs(UnmanagedType.Bool)]
        public bool bRestartable;
    }

    [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
    static extern int RmRegisterResources(uint pSessionHandle,
                                          UInt32 nFiles,
                                          string[] rgsFilenames,
                                          UInt32 nApplications,
                                          [In] RM_UNIQUE_PROCESS[] rgApplications,
                                          UInt32 nServices,
                                          string[] rgsServiceNames);

    [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)]
    static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey);

    [DllImport("rstrtmgr.dll")]
    static extern int RmEndSession(uint pSessionHandle);

    [DllImport("rstrtmgr.dll")]
    static extern int RmGetList(uint dwSessionHandle,
                                out uint pnProcInfoNeeded,
                                ref uint pnProcInfo,
                                [In, Out] RM_PROCESS_INFO[] rgAffectedApps,
                                ref uint lpdwRebootReasons);

    /// <summary>
    /// Find out what process(es) have a lock on the specified file.
    /// </summary>
    /// <param name="path">Path of the file.</param>
    /// <returns>Processes locking the file</returns>
    /// <remarks>See also:
    /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373661(v=vs.85).aspx
    /// http://wyupdate.googlecode.com/svn-history/r401/trunk/frmFilesInUse.cs (no copyright in code at time of viewing)
    /// 
    /// </remarks>
    static public List<Process> WhoIsLocking(string path)
    {
        uint handle;
        string key = Guid.NewGuid().ToString();
        List<Process> processes = new List<Process>();

        int res = RmStartSession(out handle, 0, key);

        if (res != 0)
            throw new Exception("Could not begin restart session.  Unable to determine file locker.");

        try
        {
            const int ERROR_MORE_DATA = 234;
            uint pnProcInfoNeeded = 0,
                 pnProcInfo = 0,
                 lpdwRebootReasons = RmRebootReasonNone;

            string[] resources = new string[] { path }; // Just checking on one resource.

            res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null);

            if (res != 0) 
                throw new Exception("Could not register resource.");                                    

            //Note: there's a race condition here -- the first call to RmGetList() returns
            //      the total number of process. However, when we call RmGetList() again to get
            //      the actual processes this number may have increased.
            res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons);

            if (res == ERROR_MORE_DATA)
            {
                // Create an array to store the process results
                RM_PROCESS_INFO[] processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded];
                pnProcInfo = pnProcInfoNeeded;

                // Get the list
                res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons);

                if (res == 0)
                {
                    processes = new List<Process>((int)pnProcInfo);

                    // Enumerate all of the results and add them to the 
                    // list to be returned
                    for (int i = 0; i < pnProcInfo; i++)
                    {
                        try
                        {
                            processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId));
                        }
                        // catch the error -- in case the process is no longer running
                        catch (ArgumentException) { }
                    }
                }
                else
                    throw new Exception("Could not list processes locking resource.");                    
            }
            else if (res != 0)
                throw new Exception("Could not list processes locking resource. Failed to get size of result.");                    
        }
        finally
        {
            RmEndSession(handle);
        }

        return processes;
    }
}

UPDATE

Here is another discussion with sample code on how to use the Restart Manager API.

By Eric J.. Read the original answer on Stack Overflow.

If this is happening to you on a production environment or with an app that you can't change, the quick fix is to empty the Temp folder.

Depending on the user that is running the application you should either

  • Empty C:\Windows\Temp (for IIS or services running under LocalSystem account)
  • Or %temp% for locally logged on users (which for me is C:\Users\MyUserName\AppData\Local\Temp).

On the other side, if your own code is throwing this, and you want to prevent this from happening ever again:

  1. Do not use System.IO.Path.GetTempFileName()!

GetTempFileName() is a wrapper of the two decades old Win32 Api. It generate file names that will very easily collide. It circumvents those collitions by heavily looping on the file system, iterating possible file names from "%temp%\tmp0000.tmp" to "tmpFFFF.tmp" and skipping already existing ones. This is a I/O intensive, slow, and frankly terrible algorithm. Also using only 4 hex characters is what makes the artificial limit of 65536 files before failing.

The alternative is to generate file names that will not collide. For example, lets reuse GUID's logic: 32 hex digits will almost never collide.

private string GetTempFileName()
{
    return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
}
// Sample: c:\Windows\Temp\2e38fe87-f6bb-4b0d-90b3-2d07016324c1

This expands the limit from 65k to 4k millions files max (theoretically)... Of course, having leaked 65k files is already terrible, so...

  1. Do not leak temp files!

Double check your app for all happy and unhappy paths (like unexpected exceptions). Ensure it's correctly disposing each FileStream and deleting the temp files in Finally blocks .

  1. Clean the temp folder

Clean it now, and educate the system administrator to clean it periodically, because you can't trust every app in the wild. On my own servers I would automate this task using:

  • For global Windows\Temp

schtasks /Create /TR "cmd /c call DEL /F /S /Q %^TEMP%" /TN "Delete Global Temp Files" /sc WEEKLY /ST 12:00 /ru system

  • For current user:

schtasks /Create /TR "cmd /c call DEL /F /S /Q %^TEMP%" /TN "Delete %username% Temp Files" /sc WEEKLY /ST 12:00

By Gerardo Grignoli. Read the original answer on Stack Overflow.