ERROR_IO_INCOMPLETE (0x000003E4, decimal 1004) isn't a crash. It's Windows telling you: "You asked me about that overlapped I/O operation, but it hasn't finished yet." That's it. The operation might still complete successfully a millisecond later. The problem is that the calling code treated a pending result as a failure.
You'll see this in Event Viewer, in Windows application logs from backup software, in SAN/NAS clients, in database engines (SQL Server, MySQL on Windows), and any app that uses overlapped reads/writes on files or named pipes. If you're a sysadmin and a backup job starts throwing 0x3E4, don't panic about disk failure. The culprit is almost always one of three things.
Cause 1: App polls overlapped I/O without waiting — the most common case
Overlapped I/O is asynchronous by design. Call ReadFile or WriteFile with an OVERLAPPED structure, and Windows returns immediately with ERROR_IO_PENDING (997). You then have to wait — either with WaitForSingleObject on the event handle in the OVERLAPPED struct, or with an I/O completion port. Only then do you call GetOverlappedResult.
If a developer skips the wait and calls GetOverlappedResult too early, Windows returns ERROR_IO_INCOMPLETE. The operation is fine. The code is wrong.
Classic real-world trigger: a third-party backup agent targeting a network share. The share is on a slow link (VPN, WAN, or a busy NAS), and the agent's polling interval is 100ms. On fast local disk, the read finishes in 20ms and everything looks fine. Put that same agent on a 40ms-latency link and suddenly every read returns 0x3E4. The agent logs an error, the backup fails, and someone files a ticket about "corrupt storage."
If it's your code, the fix is to actually wait:
OVERLAPPED ov = {0};
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
BOOL ok = ReadFile(hFile, buf, len, NULL, &ov);
if (!ok) {
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING) {
// THIS is the part people skip
WaitForSingleObject(ov.hEvent, INFINITE);
} else {
// real error
}
}
DWORD bytes = 0;
if (!GetOverlappedResult(hFile, &ov, &bytes, FALSE)) {
// now you can trust the error code
}
If it's not your code, patch or upgrade the app. Don't waste time on driver updates — they won't help. The bug is in userland.
Cause 2: Network path dropped mid-I/O on a file share or named pipe
Overlapped reads and writes over SMB or named pipes can return 0x3E4 when the underlying transport gets yanked. The handle is still technically valid, but the pending I/O will never signal because the session is dead. Applications that poll see ERROR_IO_INCOMPLETE forever instead of ERROR_NETNAME_DELETED (64) or ERROR_CONNECTION_ABORTED (1236).
You see this on:
- Wi-Fi laptops that roam between APs mid-write to a mapped drive
- VPN clients that reconnect while an app has an open handle
- NAS devices rebooting or failing over to a secondary controller
- Hyper-V live migration where a VM's vhdx sits on a CSV that briefly drops
The fix, in order:
- Close and reopen the handle. There's no recovering a pending I/O whose transport is gone.
- Check
net useand the SMB session state:Get-SmbSessionin PowerShell. If the session is stale, remove the mapped drive and remap. - For VPN users, stop putting file shares over flaky tunnels. Use a sync tool or DFS-R instead of live SMB. This is a design fix, not a registry tweak.
- For NAS failover, shorten the SMB timeout so the client fails fast instead of hanging on dead I/O. Set
SessTimeoutunderHKLM\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters. Default is 45 seconds. Cut it to 15 if you're on a redundant path.
Don't bother disabling SMB signing or tweaking oplocks unless you've confirmed the session is dropping. That stuff rarely helps and usually causes new problems.
Cause 3: Named pipe server stalls or client disappears
Named pipes are overlapped I/O too. If a server process is holding a pipe handle and gets stuck (deadlock, GC pause, thread pool exhaustion), clients polling for completion hit 0x3E4. Same story if the client process exits while a write is still pending — the server's GetOverlappedResult keeps returning ERROR_IO_INCOMPLETE because the completion will never fire.
Common triggers: SQL Server Agent jobs using named pipe connections, Windows services talking to each other via pipes, and any homegrown IPC over \\.\pipe\. I've seen this with older monitoring agents that open a pipe to a service and don't handle the service restarting.
How to confirm: run handle.exe from Sysinternals against the process, look for pipe handles, and check whether the counterpart process is alive. If the client's gone, the pipe handle is orphaned and the server needs to close it and listen again.
For your own pipe code: always register a cleanup path that closes the pipe handle on client disconnect. Use DisconnectNamedPipe then ConnectNamedPipe again. Don't rely on the overlapped event firing — it won't if the other end vanished.
What doesn't cause ERROR_IO_INCOMPLETE
- Bad RAM. You'd get 0x0000000A or 0xC0000005, not 0x3E4.
- Failing disks. That's usually 0x0000007B or I/O errors surfaced as 5/23/1117.
- Missing Visual C++ runtimes. That gives you a totally different failure mode.
- Antivirus. Occasionally AV filter drivers delay I/O, but they don't return 0x3E4 unless the app was already mishandling async completion.
If someone tells you to run sfc /scannow, humor them, but it won't fix this. ERROR_IO_INCOMPLETE is a programming or session-state problem, not a corrupted-system-file problem.
Quick-reference summary
| Cause | Symptom | Fix |
|---|---|---|
| Polling overlapped I/O before completion | App logs 0x3E4 but operation eventually succeeds | Add WaitForSingleObject or IOCP before GetOverlappedResult |
| Network share or SMB session dropped | Error appears after roam, VPN reconnect, or NAS failover | Close/reopen handle, remap share, shorten SessTimeout |
| Named pipe server stalled or client gone | Pipe-based service hangs, error repeats | Close orphaned pipe handle, re-listen; fix server deadlock |
Bottom line: 0x3E4 is almost never a hardware problem. It's a caller asking "are we there yet" before the car left the driveway, or a session that quietly died. Find out which one you're dealing with and the fix is usually a code change or a reconnect — not a rebuild.