dotnet/AspNetCore.Docs · error · Error

worker exports not loaded

Error message

worker exports not loaded

What it means

The Blazor web-worker script sets `startupError` if `dotnet.create()` / `getAssemblyExports` fails, then in the message handler throws `startupError || 'worker exports not loaded'` when `assemblyExports` is still falsy. The literal fallback fires when the worker never obtained .NET assembly exports and no specific startup error was captured.

Source

Thrown at aspnetcore/blazor/blazor-with-dotnet-on-web-workers.md:326

```javascript
import { dotnet } from '../_framework/dotnet.js';

let assemblyExports;
let startupError;

try {
  const { getAssemblyExports, getConfig } = await dotnet.create();
  const config = getConfig();
  assemblyExports = await getAssemblyExports(config.mainAssemblyName);
} catch (err) {
  startupError = err.message;
}

self.addEventListener('message', async e => {
  try {
    if (!assemblyExports) {
      throw new Error(startupError || 'worker exports not loaded');
    }

    let result;
    switch (e.data.command) {
      case 'generateQR':
        result = assemblyExports.QRGenerator.Generate(e.data.text, e.data.size);
        break;
      default:
        throw new Error(`Unknown command: ${e.data.command}`);
    }

    self.postMessage({ command: 'response', 
      requestId: e.data.requestId, result });
  } catch (err) {
    self.postMessage({ command: 'response', 
      requestId: e.data.requestId, error: err.message });
  }
});

View on GitHub (pinned to c67a80103a)

Solutions

  1. Have the main thread wait for a ready signal from the worker (e.g. worker posts `{command:'ready'}` after `getAssemblyExports`) before sending commands.
  2. Ensure `Generate` is annotated `[JSExport]` and the project sets `<AllowLocation>true</AllowLocation>` / proper runtime assets so exports are discovered.
  3. Verify the published `mainAssemblyName` matches the assembly containing `QRGenerator`.
  4. Inspect `startupError` (post it back in the catch) to distinguish boot failure from a race.

Example fix

// before
worker.postMessage({ command: 'generateQR', text, size, requestId });

// after
await waitForWorkerReady(worker);
worker.postMessage({ command: 'generateQR', text, size, requestId });
Defensive patterns

Strategy: validation

Validate before calling

// Main thread: wait for worker ready before sending commands
function postWhenReady(worker, msg) {
  return new Promise((resolve, reject) => {
    const onReady = (e) => {
      if (e.data.command === 'ready') {
        worker.removeEventListener('message', onReady);
        worker.postMessage(msg);
        resolve();
      }
    };
    worker.addEventListener('message', onReady);
  });
}

Type guard

function workerReady(exports) {
  return exports != null && typeof exports.QRGenerator?.Generate === 'function';
}

Try / catch

// In the worker message handler the throw is intentional; surface it:
self.postMessage({ command: 'response', requestId: e.data.requestId, error: err.message });
// Main thread:
if (resp.error && /worker exports not loaded/.test(resp.error)) retryAfterDelay();

Prevention

When it happens

Trigger: Posting a message to the worker before (or after a failed) `dotnet.create()` completed such that `assemblyExports` is null/undefined. Also if `getAssemblyExports` resolved to nothing or the config's mainAssemblyName was wrong.

Common situations: Worker boot race — main thread posts `generateQR` before startup finished; missing `[JSExport]` attribute on `Generate`; mainAssemblyName mismatch so `assemblyExports.QRGenerator` is undefined; .NET runtime resource (wasm/dll) failed to fetch (404/CORS).

Related errors


AI-assisted analysis of dotnet/AspNetCore.Docs@c67a80103a (2026-08-13). Data as JSON: /api/errors/77ed506efb5b0b33. Report an issue: GitHub.