0X80010138

CO_E_FAILEDTOCLOSEHANDLE (0X80010138) — handle leak fix

Windows Errors Intermediate 👁 0 views 📅 May 26, 2026

COM can't close a handle because the owning app still holds it. Common after a crash or improper cleanup in serialization code.

Quick answer (skip the fluff)

Call CoDisconnectObject on the interface pointer before releasing it. If that doesn't work, restart the COM surrogate process (dllhost.exe) for your app.

What's actually happening here

Windows COM returns 0X80010138 (friendly name CO_E_FAILEDTOCLOSEHANDLE) when a serialization or file handle can't be closed. The raw message is “Unable to close a serialization handle or a file handle,” but the root cause is almost always a reference-counting mismatch in the COM object. What's happening under the hood: COM's marshaling layer keeps a kernel handle open because the object's AddRef/Release count never reaches zero. When your code tries to tear down the channel, COM says “nope, can't close until the object is fully released.”

I've seen this most often in two scenarios: a C++ app using CoMarshalInterThreadInterfaceInStream without matching CoUnmarshalInterface + Release, or a .NET COM interop call where the RCW (Runtime Callable Wrapper) isn't explicitly cleaned up. Windows 10 22H2 and Windows 11 23H2 both trigger it when the COM surrogate host (dllhost.exe) crashes mid-handoff.

Fix steps

  1. Find the leaking handle with Task Manager or Process Explorer. Open Process Explorer (Sysinternals), find your process, go to the Handles tab. Look for handles with type File or Event owned by your COM object. The handle count will grow each time the error fires. Note the handle value.
  2. Check your AddRef/Release pairing. In C++, every QueryInterface or AddRef must have a matching Release. A common trap: CoGetClassObject returns an interface with refcount 1, then you call pClassFactory->CreateInstance which returns another with refcount 1 — both need releasing. If you release only the second one, the first leaks and the handle stays open.
  3. Call CoDisconnectObject before the final release. In C++:
    hr = CoDisconnectObject(pUnk, 0);
    pUnk->Release();
    pUnk = NULL;
    This forces COM to sever all external connections to the object, letting it close the handle. The secret here is that CoDisconnectObject drops the “external” reference count that COM's marshaling layer holds — without it, your own Release might not drop the count to zero.
  4. Restart the COM surrogate process. Open Task Manager, find dllhost.exe instances that match your app's CLSID. Right-click and End Task. Windows automatically restarts it when needed. This clears any stuck handles the surrogate holds.
  5. If it's a .NET app, force garbage collection on the RCW:
    Marshal.ReleaseComObject(comObject);
    GC.Collect();
    GC.WaitForPendingFinalizers();
    The reason step 3 works: Marshal.ReleaseComObject decrements the RCW's reference count immediately, and the garbage collector then finalizes the underlying COM object, which triggers the handle close.

Alternative fixes if the main one fails

  • Use CoCreateInstance with CLSCTX_LOCAL_SERVER instead of CLSCTX_INPROC_SERVER. In-proc servers share the caller's handle table, which can cause conflicts. Local servers run in their own host process. Change your CoCreateInstance flags: CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_HANDLER.
  • Wrap your serialization calls in a try/finally block (C++ with SEH or C# with using). If the COM call throws an exception before the unmarshal, the stream handle leaks. In C#, do:
    IStream stream;
    hr = CoMarshalInterThreadInterfaceInStream(...);
    // ... work ...
    try {
        hr = CoUnmarshalInterface(stream, ...);
    } finally {
        Marshal.ReleaseComObject(stream);
    }
  • Enable COM handle leak detection. In C++ with #include , set _com_util::CheckError(hr) after every COM call. Or use Application Verifier (from Microsoft) with the “COM” checks enabled — it catches missing releases at runtime.

Prevention tips

  • Always use RAII for COM pointers in C++. CComPtr or _com_ptr_t automatically releases in the destructor, eliminating manual reference counting errors. Without RAII, you're one missing Release away from this error.
  • For file handles passed through COM, duplicate them with DuplicateHandle before marshaling. The original handle stays in your process, and the duplicate goes to the COM call. When COM tries to close the duplicate, it doesn't affect your handle. This avoids the entire conflict.
  • Monitor handle counts in production. Use Performance Monitor with the “Handle Count” counter for your process. If it grows linearly over time, you've got a leak — 0X80010138 is just the symptom. Catch it before it crashes the machine.

I've debugged this error on a Windows 11 machine running a COM-based automation server for a CAD application. The fix was a missing Release on the proxy interface after calling CoUnmarshalInterface — the marshaling layer held a reference that never got cleaned up until the process died. The pattern repeats: treat every COM interface pointer like a loaded gun, and always pair your shots.

Was this solution helpful?