wailsapp/wails · error · DOMException
InvalidStateError
InvalidStateError
Error message
Still in CONNECTING state.
What it means
GlobalFree releases an HGLOBAL block; it returns NULL on success and the handle itself on failure. The w32 wrapper panics with 'GlobalFree failed' when the return is non-zero. The documented failure causes are an invalid handle (already freed or never allocated) or a locked object: memory allocated GMEM_MOVEABLE cannot be freed while its lock count from GlobalLock is non-zero — each GlobalLock must be followed by GlobalUnlock before freeing.
Source
Thrown at v3/internal/runtime/desktop/@wailsio/runtime/src/stream.ts:277
// Fire and forget: the ack arrives as an open frame on the poll, which
// is what moves readyState to OPEN.
this._chain = this._chain.then(() =>
postFrame(this._id, KIND_OPEN, new Uint8Array(0), name, this._openAbort.signal)
).catch((err) => {
this._fail(err);
});
startPolling();
}
/**
* Send a frame to Go. Accepts anything a WebSocket does; strings are
* encoded as UTF-8, since Go receives every frame as a byte slice.
*/
send(data: string | ArrayBufferLike | ArrayBufferView | Blob): void {
if (this.readyState === WailsSocket.CONNECTING) {
// Matches the WebSocket specification.
throw new DOMException("Still in CONNECTING state.", "InvalidStateError");
}
if (this.readyState !== WailsSocket.OPEN) {
return;
}
// Resolve now rather than inside the chain. Mutable binary inputs are
// copied synchronously so send() snapshots their bytes like a native
// WebSocket, even when a batch waits behind an in-flight request. A
// Blob is immutable and is therefore safe to read asynchronously once.
const immediate = toBytesSync(data);
const snapshot = immediate
? Promise.resolve(immediate)
: toBytes(data, this._sendAbort.signal);
// close() may discard this promise before the flush has dequeued it.
// Attach a rejection observer immediately so cancelling an asynchronous
// Blob read cannot become an unhandled rejection; Promise.all still
// observes the original rejection when the flush already owns it.
void snapshot.catch(() => {});View on GitHub (pinned to 0e754b1b40)
Solutions
- Ensure every GlobalLock has a matching GlobalUnlock before GlobalFree; call GlobalUnlock and let its FALSE return tell you the lock count hit zero.
- Never GlobalFree an HGLOBAL after passing it to SetClipboardData — ownership transfers to the system.
- Set the handle variable to zero after freeing to make double-free bugs obvious in review.
- Verify the handle came from GlobalAlloc (or returned by an API documented to produce freeable HGLOBALs) and not another allocator.
Example fix
// before p := w32.GlobalLock(h) copy(dst, sliceFrom(p)) w32.GlobalFree(h) // still locked -> panic // after p := w32.GlobalLock(h) copy(dst, sliceFrom(p)) w32.GlobalUnlock(h) w32.GlobalFree(h)
Defensive patterns
Strategy: validation
Prevention
- Call GlobalUnlock until it returns FALSE (lock count zero) before GlobalFree.
- Never free an HGLOBAL already handed to SetClipboardData — the system owns it.
- Zero out handle variables after free to expose double-frees in testing.
When it happens
Trigger: Calling GlobalFree on a block that is still locked (GlobalUnlock not called or returned FALSE because of a prior unlock imbalance); double-freeing the same HGLOBAL; freeing a handle that ownership was transferred away from (e.g. already passed to SetClipboardData, which makes the system the owner).
Common situations: Clipboard code that GlobalLocks a buffer, copies data, SetClipboardData's it, then also GlobalFrees it — after hand-off to the clipboard that is a double-managed handle; error paths that skip GlobalUnlock but still reach GlobalFree.
Related errors
- ${await resp.text()}
- ${await resp.text()}
- CreateStreamOnHGlobal failed with E_INVALIDARG
- Invoke SysFreeString error.
- Invalid JSON passed to callback: ${e.message}. Message: ${in
AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15).
Data as JSON: /api/errors/fb42f4017bf491a8.
Report an issue: GitHub.