dotnet/runtime · error · Error
External roots are not supported in multithreaded mode
Error message
External roots are not supported in multithreaded mode
What it means
Thrown by mono_wasm_new_external_root when Wasm threads are enabled and disableManagedTransition is on. External roots wrap an externally-controlled native address (typically on the managed stack); that pattern is incompatible with the threaded runtime's GC coordination, so the API refuses to create one. The message variant 'in multithreaded mode' distinguishes it from the root-buffer guard.
Source
Thrown at src/mono/browser/runtime/roots.ts:48
capacity = capacity | 0;
const capacityBytes = capacity * 4;
const offset = malloc(capacityBytes);
if ((<any>offset % 4) !== 0)
throw new Error("Malloc returned an unaligned offset");
_zero_region(offset, capacityBytes);
return new WasmRootBufferImpl(offset, capacity, true, name);
}
/**
* Allocates a WasmRoot pointing to a root provided and controlled by external code. Typicaly on managed stack.
* Releasing this root will not de-allocate the root space. You still need to call .release().
*/
export function mono_wasm_new_external_root<T extends MonoObject> (address: VoidPtr | MonoObjectRef): WasmRoot<T> {
if (WasmEnableThreads && runtimeHelpers.disableManagedTransition) throw new Error("External roots are not supported in multithreaded mode");
let result: WasmExternalRoot<T>;
if (!address)
throw new Error("address must be a location in the native heap");
if (_external_root_free_instances.length > 0) {
result = _external_root_free_instances.pop()!;
result._set_address(address);
} else {
result = new WasmExternalRoot<T>(address);
}
return result;
}
/**
* Allocates temporary storage for a pointer into the managed heap.
* Pointers stored here will be visible to the GC, ensuring that the object they point to aren't moved or collected.View on GitHub (pinned to 60108ba66e)
Solutions
- Avoid mono_wasm_new_external_root in threaded builds; use managed-transition-safe handle APIs.
- Run with the single-threaded build if external roots are mandatory for the interop scenario.
- Refactor the caller to obtain GC roots through mono_wasm_new_root / scratch roots that are compatible with the runtime mode.
Defensive patterns
Strategy: validation
Validate before calling
import WasmEnableThreads from 'consts:wasmEnableThreads';
import { runtimeHelpers } from './globals';
function canUseExternalRoot(): boolean {
return !(WasmEnableThreads && runtimeHelpers.disableManagedTransition);
} Prevention
- Branch interop code by runtime mode before creating external roots.
- Document which binding paths are single-threaded-only.
- Run threaded-mode tests that exercise interop to catch regressions early.
When it happens
Trigger: Calling mono_wasm_new_external_root(address) in a threaded dotnet Wasm build with disableManagedTransition active. The threaded-mode guard at line 48 fires before the address is even inspected.
Common situations: Code that wraps stack/native addresses as roots, ported to the threaded runtime; an interop layer that previously used external roots now running with pthreads enabled; tests targeting the threaded configuration hitting legacy binding paths.
Related errors
- External roots are not supported when threads are enabled
- Cannot call synchronous C# methods.
- Cannot call synchronous C# method from inside a synchronous
- Invalid jsThreadBlockingMode
- capacity >= 1
AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10).
Data as JSON: /api/errors/8d8308b47f9fe252.
Report an issue: GitHub.