wailsapp/wails · error · Error

${await resp.text()}

Error message

${await resp.text()}

What it means

GlobalLock maps an HGLOBAL (allocated GMEM_MOVEABLE) into a pointer and increments its lock count; it returns NULL on failure. The w32 wrapper panics with 'GlobalLock failed' on NULL. Failure occurs when the handle is invalid/already freed, or when the block's lock count already exceeds its maximum — in practice an unbalanced lock/unlock loop, a freed handle, or locking memory that was allocated GMEM_FIXED (where lock semantics differ and NULL signals misuse on old code paths).

Source

Thrown at v3/internal/runtime/desktop/@wailsio/runtime/src/stream.ts:699

// leaves the request slot free, which is what stops a backed-up connection from
// starving the window's poll.
async function postWithRetry(headers: Record<string, string>, body: Uint8Array, signal?: AbortSignal): Promise<void> {
    // Never retry with zero delay. The receiver being behind is a condition
    // that takes time to clear, and an immediate retry is a busy loop of
    // fetches — each one a scheme-handler round trip, and on the host a cgo
    // call. Start at 1 ms and ramp.
    let wait = 1;
    for (;;) {
        if (signal?.aborted) throw new StreamRequestCancelled();
        const resp = await fetch(streamURL("send"), {
            method: "POST",
            headers,
            body: body as BodyInit,
            signal,
        });
        if (resp.ok) return;
        if (resp.status !== 429) {
            throw new Error(await resp.text());
        }
        await sleep(wait, signal);
        wait = Math.min(wait * 2, 50);
    }
}

// postBatch sends several data frames for one connection in bounded requests.
// Body: count u32, then count x ( len u32 | payload ). The combined body stays
// within CHUNK_THRESHOLD: several individually small frames must not recreate
// the WebView2 body-size problem that chunking single large frames avoids.
async function postBatch(connID: number, frames: Uint8Array[], signal?: AbortSignal): Promise<void> {
    let start = 0;
    while (start < frames.length) {
        if (frames[start].byteLength > CHUNK_THRESHOLD) {
            await postFrame(connID, KIND_DATA, frames[start], undefined, signal);
            start++;
            continue;
        }

View on GitHub (pinned to 0e754b1b40)

Solutions

  1. Pair every GlobalLock with a GlobalUnlock via defer, immediately after the lock succeeds.
  2. Verify the handle is non-zero and currently owned by your code before locking.
  3. For clipboard reads, keep the sequence tight: OpenClipboard, GetClipboardData, GlobalLock, copy out, GlobalUnlock, CloseClipboard.
  4. Null-check the source of the handle (e.g. GetClipboardData can return 0) before attempting the lock.

Example fix

// before
p := w32.GlobalLock(h)
process(p)
if err != nil { return } // GlobalUnlock never called
w32.GlobalUnlock(h)

// after
p := w32.GlobalLock(h)
if p == nil { return errLockFailed }
defer w32.GlobalUnlock(h)
process(p)
return nil
Defensive patterns

Strategy: validation

Try / catch

defer func() {
	if r := recover(); r != nil {
		if msg, _ := r.(string); strings.HasPrefix(msg, "GlobalLock failed") {
			log.Printf("clipboard lock failed: stale handle")
			return
		}
		panic(r)
	}
}()
p := w32.GlobalLock(h)

Prevention

When it happens

Trigger: Locking an HGLOBAL that was already GlobalFree'd; calling GlobalLock repeatedly in a retry loop without GlobalUnlock; locking a handle obtained from GetClipboardData after the clipboard has moved on and the handle is stale.

Common situations: Clipboard paste features: GetClipboardData → GlobalLock → process → forget GlobalUnlock; error branches that skip the unlock; holding the lock across OpenClipboard/CloseClipboard boundaries incorrectly.

Related errors


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