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
- Pair every GlobalLock with a GlobalUnlock via defer, immediately after the lock succeeds.
- Verify the handle is non-zero and currently owned by your code before locking.
- For clipboard reads, keep the sequence tight: OpenClipboard, GetClipboardData, GlobalLock, copy out, GlobalUnlock, CloseClipboard.
- 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
- Pair every GlobalLock with a deferred GlobalUnlock.
- Null-check the handle source (GetClipboardData) before locking.
- Keep clipboard read sequences tight: open, get, lock, copy, unlock, close.
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
- ${await resp.text()}
- InvalidStateError
- Invalid JSON passed to callback: ${e.message}. Message: ${in
- Callback '${callbackID}' not registered!!!
- Invalid JSON passed to Notify: ${notifyMessage}
AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15).
Data as JSON: /api/errors/e6f1aaa5fb043fea.
Report an issue: GitHub.