dotnet/runtime · error · Error

GC is not locked

Error message

GC is not locked

What it means

Thrown by mono_wasm_gc_unlock() in gc-lock.ts when gc_locked is false, i.e. an unlock with no prior lock. The lock is stateful (gc_locked flag) and not reference-counted, so unbalanced unlocks are caught immediately to prevent the native GC lock from being released while not held.

Source

Thrown at src/mono/browser/runtime/gc-lock.ts:28

// TODO https://github.com/dotnet/runtime/issues/100411
// after Blazor stops using mono_wasm_gc_lock, mono_wasm_gc_unlock

export function mono_wasm_gc_lock (): void {
    if (gc_locked) {
        throw new Error("GC is already locked");
    }
    if (WasmEnableThreads) {
        if (ENVIRONMENT_IS_PTHREAD) {
            throw new Error("GC lock only supported in main thread");
        }
        cwraps.mono_wasm_gc_lock();
    }
    gc_locked = true;
}

export function mono_wasm_gc_unlock (): void {
    if (!gc_locked) {
        throw new Error("GC is not locked");
    }
    if (WasmEnableThreads) {
        if (ENVIRONMENT_IS_PTHREAD) {
            throw new Error("GC lock only supported in main thread");
        }
        cwraps.mono_wasm_gc_unlock();
    }
    gc_locked = false;
}

View on GitHub (pinned to 290d5ab72c)

Solutions

  1. Only unlock what you locked; pair every lock with exactly one unlock.
  2. In try/finally, only unlock if the lock actually succeeded (track a local `locked` boolean).
  3. Audit early-return and throw paths to ensure the unlock is not skipped or doubled.

Example fix

// before: unlock runs even if lock threw
mono_wasm_gc_lock();
try { work(); }
finally { mono_wasm_gc_unlock(); } // throws if lock failed

// after
let locked = false;
try {
  mono_wasm_gc_lock();
  locked = true;
  work();
} finally {
  if (locked) mono_wasm_gc_unlock();
}
Defensive patterns

Strategy: validation

Validate before calling

import { gc_locked } from "./gc-lock";
let didLock = false;
function unlockGcSafe() {
  if (!gc_locked) throw new Error('unlock without lock; balance your calls');
  mono_wasm_gc_unlock();
}

Type guard

null

Try / catch

let locked = false;
try { mono_wasm_gc_lock(); locked = true; work(); }
finally { if (locked) mono_wasm_gc_unlock(); }

Prevention

When it happens

Trigger: Calling mono_wasm_gc_unlock() without a matching mono_wasm_gc_lock(). A lock that threw before setting gc_locked (e.g. the pthread or already-locked guards) followed by an unconditional unlock in a finally block.

Common situations: try/finally that always unlocks even when the lock call itself threw. Early returns between lock and unlock. Two routines where one unlocks the other's lock.

Related errors


AI-assisted analysis of dotnet/runtime@290d5ab72c (2026-08-06). Data as JSON: /api/errors/8da488f3e9cdf0b9. Report an issue: GitHub.