I know how frustrating it is when your app crashes with 0x0000059B right after you thought everything was working — let's get it sorted out.
ERROR_DESTROY_OBJECT_OF_OTHER_THREAD (0x0000059B) means exactly what it says: a thread tried to destroy a window, GDI object, or USER object that was created by a different thread. Windows enforces thread ownership on GUI objects. Your cleanup code is running on the wrong thread. The real fix is to route the destruction back to the thread that created the object.
The fix: post the destroy back to the owning thread
Don't call DestroyWindow (or DeleteObject on a GUI handle) directly from a worker thread. Instead, ask the owning thread to do it. The owning thread has a message pump — use it.
If your worker thread just wants the window gone, post a close request:
// From the worker thread:
if (!PostMessage(hwnd, WM_CLOSE, 0, 0)) {
// PostMessage can fail if the target thread is hung
DWORD err = GetLastError();
// log err — usually ERROR_INVALID_WINDOW_HANDLE (1400)
}
After that call returns, you should see PostMessage return a nonzero value. The worker thread keeps running; the UI thread will pick up WM_CLOSE on its next GetMessage loop and call DestroyWindow itself. That's the whole trick.
If you need a custom teardown order (release a lock, unregister a hotkey, then destroy), define your own message and pass any data you need:
#define WM_APP_DESTROY_SELF (WM_APP + 1)
// Worker thread:
PostMessage(hwnd, WM_APP_DESTROY_SELF, 0, 0);
// UI thread WndProc:
case WM_APP_DESTROY_SELF:
UnregisterHotKey(hwnd, 1);
DestroyWindow(hwnd);
return 0;
After wiring this up and rebuilding, the 0x59B error should disappear. If it doesn't, you've got a second call site somewhere — search the whole codebase for DestroyWindow, EndDialog, DestroyMenu, and DeleteObject and check which thread each one runs on.
For pure GDI objects (pens, brushes, bitmaps), the same rule applies but the fix is different: don't share the handle across threads at all. Create the object on the thread that will use it, delete it there, and pass data — not handles — between threads.
Why this happens
Windows GUI objects (HWND, HMENU, HDC on a window, most USER handles) have thread affinity. The kernel stores the owning thread ID inside the object header. When a different thread calls a destroy function, the USER subsystem notices the mismatch and returns 0x59B instead of letting the object disappear.
This isn't arbitrary. If a non-owner thread could destroy a window, the owning thread's message queue could be mid-dispatch on that same HWND — you'd get a use-after-free inside DispatchMessage. Microsoft added this check in the NT line specifically to stop that class of crash. It's a safety feature, and it's saving you from a worse bug.
Common triggers I see in the wild:
- A background worker finishes a job and calls
DestroyWindow(hwnd)on the main window to "clean up." Boom. 0x59B. - A
std::threadlambda captures an HWND and callsEndDialogdirectly instead ofPostMessage(hwnd, WM_CLOSE, ...). - An MFC
CWinThreadobject getsdeleted from the main thread, but its window belonged to a secondary UI thread. - Direct2D or WPF interop code that creates an
HWNDon one thread and destroys it on another after anawait.
If you're seeing 0x59B specifically inside a crash dump from user32!NtUserDestroyWindow, check the debugger's thread view. The faulting thread ID won't match the window's owning thread ID. That's your confirmation.
Less common variations
Destroying a menu or accelerator table from the wrong thread
DestroyMenu and DestroyAcceleratorTable follow the same rule. If you load a menu on the UI thread and try to free it during shutdown on a different thread, you'll get 0x59B. Marshal the cleanup through PostMessage and handle it in WM_DESTROY.
COM apartment issues that masquerade as this error
If your object is an OLE/ActiveX control, the owning thread is the STA that created it. Calling Release from an MTA thread can surface 0x59B because the final release triggers DestroyWindow internally. Fix: marshal the interface pointer with CoMarshalInterThreadInterfaceInStream and release it on the original apartment, or use CoDisconnectObject.
Timers and child windows
A SetTimer callback fires on the thread that set the timer. If you cancel a timer and destroy its child window from a different thread, same error. Kill the timer from the same thread that created it, then post a destroy request.
Third-party libraries that hide the thread hop
Some UI frameworks spin up their own threads for rendering, then hand you a callback on that thread. If your callback tears down UI objects, you'll hit 0x59B. The workaround is to marshal the teardown back to the framework's UI thread via whatever Invoke or Post primitive it exposes. Read the framework's threading docs — this is where the "it worked fine on my machine" bug reports come from.
Prevention
Adopt one rule and stick to it: the thread that creates a GUI object is the only thread that may destroy it. Write it on a sticky note. Put it in your code review checklist.
Practical habits that stop 0x59B before it starts:
- Never pass raw HWNDs to worker threads for the purpose of destruction. Pass a signal — an event, a message, a callback that posts a message.
- Wrap window creation and destruction in a small RAII class that asserts
GetCurrentThreadId() == m_ownerThreadIdon destruction. You'll catch the bug at the call site instead of in a crash dump. - For MFC, use
PostMessagewithWM_CLOSEand let the message map handle cleanup. Don'tdeleteaCWndfrom another thread. - In C++/WinRT and WPF interop, treat the dispatcher/UI thread as the only authority for UI teardown. If you're in an
asyncmethod, hop back withDispatcherQueue.TryEnqueue. - Turn on Application Verifier with the "Basics" and "Handles" checks during testing. It'll flag cross-thread handle misuse before it reaches production.
If you fix the call site and the error persists, you've got a leak — another thread is holding a stale HWND from a previous window instance. Attach a debugger, break on NtUserDestroyWindow, and check the owning thread ID in the object header. That'll tell you exactly which code path is lying about which thread it's on.