dotnet/runtime · error · Error

GC is already locked

Error message

GC is already locked

What it means

Thrown by mono_wasm_gc_lock() in gc-lock.ts when the module-level gc_locked flag is already true. This lock is deliberately NOT re-entrant: it exists to batch GC-sensitive operations, so a second lock without an intervening unlock is a programming error. In the threads-enabled build it also drives the native mono_wasm_gc_lock().

Source

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

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

import WasmEnableThreads from "consts:wasmEnableThreads";
import { ENVIRONMENT_IS_PTHREAD } from "./globals";
import cwraps from "./cwraps";

export let gc_locked = false;

// 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");
        }

View on GitHub (pinned to 290d5ab72c)

Solutions

  1. Ensure every mono_wasm_gc_lock() has a matching mono_wasm_gc_unlock() (use try/finally).
  2. Do not nest lock callers; if an inner routine may already be inside a lock, do not lock again.
  3. If you need nesting, track ownership yourself and skip re-locking rather than calling the non-reentrant API twice.

Example fix

// before
mono_wasm_gc_lock();
doWorkThatAlsoLocks(); // throws: already locked
mono_wasm_gc_unlock();

// after
mono_wasm_gc_lock();
try {
  doWorkWithoutLocking();
} finally {
  mono_wasm_gc_unlock();
}
Defensive patterns

Strategy: validation

Validate before calling

import { gc_locked } from "./gc-lock";
function lockGcSafe() {
  if (gc_locked) throw new Error('gc already locked by this caller; fix unbalanced lock/unlock');
  mono_wasm_gc_lock();
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Calling mono_wasm_gc_lock() twice in a row without mono_wasm_gc_unlock() between them. A code path that locks, calls into another routine that also locks, and neither tracks nesting.

Common situations: Two independent features both calling gc_lock around their work and one invoking the other. Missing/unreached unlock after an early return or thrown error. Async code that locks, awaits, and another branch locks again.

Related errors


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