Cause #1: Calling CoInitialize twice on the same thread
What's actually happening here is that your code (or a library you call) invokes CoInitialize or OleInitialize on a thread that already has COM initialized. The first call succeeds, sets up the apartment, and returns S_OK. The second call on the same thread—unless you use CoInitializeEx with a matching flag—returns this error: 0x80004012.
This error is common in plugin-based apps, like Excel add-ins or Unity scripts, where the host process initializes COM before your code runs. You don't see the first call because it's buried in the host's startup sequence.
The fix
Stop blindly calling CoInitialize at the top of every function. Instead, check the return code and ignore RPC_E_CHANGED_MODE and S_FALSE—those are fine. But CO_E_INIT_ONLY_SINGLE_THREADED means you're calling it again, and you need to guard that.
HRESULT hr = CoInitialize(NULL);
if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) {
// handle error, but 0x80004012 is not fatal
}
// If hr == S_OK or S_FALSE, COM is ready.
// If hr == RPC_E_CHANGED_MODE, another apartment exists—don't call CoUninitialize.
The real fix is to track whether you initialized COM in the first place. Use a thread-local flag.
thread_local bool comInitialized = false;
void EnsureCOM() {
if (!comInitialized) {
HRESULT hr = CoInitialize(NULL);
if (SUCCEEDED(hr) || hr == S_FALSE) {
comInitialized = true;
}
}
}
This prevents the double call. Also, don't call CoUninitialize unless you got S_OK back from your own initialization—otherwise you'll uninitialize someone else's apartment and create a different headache.
Cause #2: Mixing CoInitialize and CoInitializeEx on the same thread
Sometimes the second call isn't a duplicate—it's a conflicting call. The thread already has an STA (single-threaded apartment) set up, and your code tries to initialize as MTA using CoInitializeEx(NULL, COINIT_MULTITHREADED). COM will reject that with RPC_E_CHANGED_MODE, not 0x80004012, but if you're using CoInitialize (which always requests STA) after an MTA was set, you'll see CO_E_INIT_ONLY_SINGLE_THREADED.
I ran into this with a C++ console app that used a third-party networking library. The library initialized COM as MTA on the main thread. Then my own code called CoInitialize to create an STA for GUI automation. Boom—0x80004012.
The fix
Use CoInitializeEx and accept whatever apartment is already there. Don't force a specific mode unless you really need it.
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // STA
if (hr == RPC_E_CHANGED_MODE) {
// Already MTA—fine, just don't uninit later.
} else if (FAILED(hr)) {
// handle other errors
}
If you depend on an STA (like for OLE or drag-and-drop), you can't just switch to MTA. In that case, run your COM-dependent code on a dedicated thread that you control, and initialize that thread as STA from scratch. The main thread can stay MTA.
Cause #3: A library or framework initializes COM behind your back
Even if you're careful, a library you link—like a UI framework, a video capture SDK, or even the Windows Shell API—can initialize COM in a static initializer or in a global object's constructor. When your code later calls CoInitialize on the same thread, you get the error without any visible second call in your own source.
I hit this with a WinForms app that loaded a custom native DLL. The DLL's DllMain did some COM setup (bad practice, but it happens). The managed side then called CoInitialize—and failed with 0x80004012. The DLL was the culprit.
The fix
Check if any loaded module is initializing COM. You can use GetModuleHandle and inspect, but that's tedious. Faster: just wrap your COM calls in a helper that detects if COM is already initialized. Use CoGetApartmentType to see if you're already in an apartment.
APTTYPE aptType;
APTTYPEQUALIFIER aptQualifier;
HRESULT hr = CoGetApartmentType(&aptType, &aptQualifier);
if (SUCCEEDED(hr)) {
// COM is already initialized—skip CoInitialize entirely.
} else {
hr = CoInitialize(NULL);
}
That's the safest approach: ask COM if it's already running before you try to start it. This sidesteps the entire class of double-initialization bugs, no matter who caused them.
One more thing: if you're in a DLL with a global object that calls CoInitialize, move that call out of the constructor and into an explicit Init() function that your DLL's consumers call. That gives you control over when COM starts.
Quick-reference summary
| Cause | Detection | Fix |
|---|---|---|
| Double CoInitialize on same thread | Stack trace shows two CoInitialize calls | Guard with thread-local flag; ignore S_FALSE/RPC_E_CHANGED_MODE |
| Mixing STA and MTA requests | CoInitializeEx returns RPC_E_CHANGED_MODE before 0x80004012 | Use CoInitializeEx; adapt to existing apartment |
| Library/global initializes COM | No direct CoInitialize in your code | Use CoGetApartmentType to check first |
The common thread here is that COM is a per-thread resource. You only get one apartment per thread, and the OS takes that seriously. Respect it, and this error disappears.