microsoft/aspire · error · Win32Exception

WaitForSingleObject failed while reading…

Error message

WaitForSingleObject failed while reading IsolatedProcess.HasExited

What it means

IsolatedProcess.HasExited on Windows is implemented by calling WaitForSingleObject(processHandle, 0) — a zero-timeout poll. WAIT_FAILED indicates the wait itself failed (usually because the process handle is no longer valid), and the code throws this Win32Exception with the underlying Win32 error instead of returning a possibly-wrong answer. WAIT_ABANDONED or other unexpected values hit the adjacent InvalidOperationException branch.

Solutions

  1. Do not read HasExited after the IsolatedProcess has been disposed; keep the instance alive for the duration of polling (wrap usage in try/finally or use await WaitForExitAsync instead).
  2. Check NativeErrorCode — ERROR_INVALID_HANDLE means the handle lifetime is the problem; audit dispose ordering in the caller.
  3. Prefer WaitForExitAsync / WaitForExit over repeated HasExited polling to avoid touching the raw handle after teardown.
  4. If polling is required, guard with ObjectDisposedException handling around the HasExited access.
  5. Update the Aspire CLI if the handle lifecycle is managed entirely by the library and the error still occurs.

Example fix

// before
isolatedProcess.Dispose();
if (isolatedProcess.HasExited) { ... }
// after
if (isolatedProcess.HasExited) { ... }
isolatedProcess.Dispose();
Defensive patterns

Strategy: try-catch

Validate before calling

// Check the object isn't disposed before polling
if (startedProcess is IsolatedProcess ip && ip is IDisposable d)
{
    // track disposal with your own flag:
    bool disposed = false; // set true in your Dispose path
    if (disposed) throw new InvalidOperationException("Cannot poll HasExited after disposal.");
}

Try / catch

try
{
    bool exited = startedProcess.HasExited;
}
catch (Win32Exception ex) when (ex.Message.Contains("WaitForSingleObject failed while reading IsolatedProcess.HasExited"))
{
    // Handle was invalid — treat as 'unknown state'; re-check lifecycle/dispose ordering
}

Prevention

When it happens

Trigger: GetHasExited is invoked by HasExitedProvider from StartWindows or StartWindowsSuppressed while the underlying process handle has become invalid — typically after the SafeProcessHandle was disposed (IsolatedProcess disposed) or the handle was closed externally — and WaitForSingleObject returns WAIT_FAILED.

Common situations: Querying HasExited after disposing the IsolatedProcess, a double-dispose race, or a corrupted/stale handle obtained in a broken environment.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/2535bd7cf5e572a6. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Cli/Processes/IsolatedProcess.Windows.cs:275

            {
                try { WindowsProcessInterop.CloseHandle(pi.hThread); } catch { }
            }
            processHandle?.Dispose();
            throw;
        }
    }

    [SupportedOSPlatform("windows")]
    private static bool GetHasExited(SafeProcessHandle processHandle)
    {
        ThrowIfDisposed(processHandle, nameof(HasExited));

        var waitResult = WindowsProcessInterop.WaitForSingleObject(processHandle, 0);
        return waitResult switch
        {
            WindowsProcessInterop.WaitObject0 => true,
            WindowsProcessInterop.WaitTimeout => false,
            WindowsProcessInterop.WaitFailed => throw new Win32Exception(Marshal.GetLastWin32Error(), "WaitForSingleObject failed while reading IsolatedProcess.HasExited"),
            _ => throw new InvalidOperationException($"Unexpected WaitForSingleObject result: 0x{waitResult:X8}"),
        };
    }

    [SupportedOSPlatform("windows")]
    private static int GetExitCode(SafeProcessHandle processHandle)
    {
        ThrowIfDisposed(processHandle, nameof(ExitCode));

        // Disambiguate STILL_ACTIVE (259) from a real 259 exit code via a zero-timeout wait.
        var waitResult = WindowsProcessInterop.WaitForSingleObject(processHandle, 0);
        if (waitResult == WindowsProcessInterop.WaitTimeout)
        {
            throw new InvalidOperationException("Process has not exited; cannot read ExitCode.");
        }
        if (waitResult == WindowsProcessInterop.WaitFailed)
        {
            throw new Win32Exception(Marshal.GetLastWin32Error(), "WaitForSingleObject failed while reading IsolatedProcess.ExitCode");

View on GitHub (pinned to 25830f84bd)