0X0000059B

Fix ERROR_DESTROY_OBJECT_OF_OTHER_THREAD (0x59B) in Windows

This error means a thread tried to delete a window or object that a different thread created. The fix is making sure the right thread does the cleanup.

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::thread lambda captures an HWND and calls EndDialog directly instead of PostMessage(hwnd, WM_CLOSE, ...).
  • An MFC CWinThread object gets deleted from the main thread, but its window belonged to a secondary UI thread.
  • Direct2D or WPF interop code that creates an HWND on one thread and destroys it on another after an await.
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:

  1. 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.
  2. Wrap window creation and destruction in a small RAII class that asserts GetCurrentThreadId() == m_ownerThreadId on destruction. You'll catch the bug at the call site instead of in a crash dump.
  3. For MFC, use PostMessage with WM_CLOSE and let the message map handle cleanup. Don't delete a CWnd from another thread.
  4. In C++/WinRT and WPF interop, treat the dispatcher/UI thread as the only authority for UI teardown. If you're in an async method, hop back with DispatcherQueue.TryEnqueue.
  5. 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.

Related Errors in Windows Errors
0X00000A45 Workstation record missing user account (0X00000A45) – fix 0X000020B1 Fix ERROR_DS_CANT_MOD_SYSTEM_ONLY (0x000020B1) in AD 0X0000012A Semaphore Flooded: Fixing 0X0000012A Error on Windows 0XC00D1B5A Fix NS_E_NOMATCHING_MEDIASOURCE (0xC00D1B5A) on Windows 10/11

Was this solution helpful?

EP
Erropedia Team
Tech Support Editors
The Erropedia editorial team researches and documents real-world tech errors from across Windows, Linux, macOS, networking, databases, cloud platforms, and more. Every solution is reviewed for accuracy and updated as software and systems evolve.