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

  1. Ensure every GlobalLock has a matching GlobalUnlock before GlobalFree; call GlobalUnlock and let its FALSE return tell you the lock count hit zero.
  2. Never GlobalFree an HGLOBAL after passing it to SetClipboardData — ownership transfers to the system.
  3. Set the handle variable to zero after freeing to make double-free bugs obvious in review.
  4. 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

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


AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15). Data as JSON: /api/errors/fb42f4017bf491a8. Report an issue: GitHub.