dotnet/aspnetcore · critical · Error

Worker .NET runtime not loaded

Error message

Worker .NET runtime not loaded

What it means

Thrown inside the worker's message listener when workerExports is empty at the time a method-call message arrives. workerExports is populated only after the .NET runtime (via the supplied dotnetJsUrl) successfully initializes the assembly and GETs its exports. An empty exports map means initialization never completed or failed; startupError holds the original init error if one exists.

Solutions

  1. Ensure the host posts the 'init' message first and awaits its acknowledgment before dispatching method calls.
  2. Inspect startupError (it is forwarded when set) — the underlying .NET runtime load error is the real cause.
  3. Verify dotnetJsUrl is reachable (network tab) and that assemblyName exactly matches the WASM build output.
  4. Check the worker's Content-Security-Policy allows 'wasm-unsafe-eval' and script-src for the runtime URL.
  5. Re-send an init message to retry; the current message will be rejected but a subsequent call will succeed once exports are populated.

Example fix

// before
self.postMessage({ method, args, requestId }); // before init → throws

// after — host waits for ready signal
const ready = new Promise(resolve => {
  worker.addEventListener('message', e => { if (e.data.type === 'ready') resolve(); });
});
worker.postMessage({ type: 'init', dotnetJsUrl, assemblyName });
await ready;
worker.postMessage({ method, args, requestId });
Defensive patterns

Strategy: validation

Validate before calling

// Host-side: track init state before posting method calls
let initialized = false, initError: string | null = null;
worker.addEventListener('message', e => {
  if (e.data.type === 'ready') initialized = true;
  if (e.data.type === 'init-error') initError = e.data.error;
});
function assertReady() {
  if (initError) throw new Error(`Worker init failed: ${initError}`);
  if (!initialized) throw new Error('Worker not initialized — send init first');
}

Type guard

type InitMessage = { type: 'init'; dotnetJsUrl: string; assemblyName: string };
type CallMessage = { method: string; args: unknown[]; requestId: string };
function isInitMessage(m: unknown): m is InitMessage {
  return (m as any)?.type === 'init';
}

Try / catch

// inside the worker's message listener:
if (Object.keys(workerExports).length === 0) {
  self.postMessage({ type: 'result', requestId, error: startupError || 'Worker .NET runtime not loaded' });
  return; // do not throw — surface to caller gracefully
}

Prevention

When it happens

Trigger: Posting a {method,args,requestId} message to the worker before posting the {type:'init', dotnetJsUrl, assemblyName} message; posting it after init failed (bad dotnetJsUrl, 404 on assemblies, WASM not supported); or a race where the method-call message arrives before initialize() resolves.

Common situations: Mis-ordered worker startup sequence from the host page; CDN/dotnetJsUrl returns 404; the assemblyName doesn't match a real assembly; the worker is reused after a failed init without re-initializing; browser blocks WASM via CSP.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/6884e303126c7c9e. Report an issue: GitHub.

Appendix: source

Thrown at src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWebWorker-CSharp/wwwroot/dotnet-web-worker.js:35

    } catch (err) {
        const errorMessage = err?.message ?? String(err);
        startupError = errorMessage;
        console.error("[Worker] Failed to initialize .NET:", err);
        self.postMessage({ type: "ready", error: errorMessage });
    }
}

self.addEventListener('message', async (e) => {
    if (e.data.type === 'init') {
        await initialize(e.data.dotnetJsUrl, e.data.assemblyName);
        return;
    }

    const { method, args, requestId } = e.data;

    try {
        if (Object.keys(workerExports).length === 0) {
            throw new Error(startupError || "Worker .NET runtime not loaded");
        }

        const fn = method.split('.').reduce((obj, part) => obj?.[part], workerExports);
        if (typeof fn !== 'function') {
            throw new Error(`Method not found: ${method}`);
        }

        const result = await fn(...args);
        self.postMessage({ type: "result", requestId, result }, collectTransferables(result));
    } catch (err) {
        self.postMessage({ type: "result", requestId, error: err?.message ?? String(err) });
    }
});

function collectTransferables(value) {
    if (ArrayBuffer.isView(value)) return [value.buffer];
    if (value instanceof ArrayBuffer) return [value];
    return [];

View on GitHub (pinned to 3600ca084e)