dotnet/aspnetcore · error · Error

Blazor has already started.

Error message

Blazor has already started.

What it means

Thrown by boot() in Boot.Web.ts (the blazor.web.js entry, used by Blazor Web App SSR + interactive) when its module-level 'started' flag is already true. blazor.web.js is single-shot: it sets up streaming render, enhanced navigation, form validation, and the root component manager in one pass.

Source

Thrown at src/Components/Web.JS/src/Boot.Web.ts:39

import { hasProgrammaticEnhancedNavigationHandler, performProgrammaticEnhancedNavigation } from './Services/NavigationUtils';
import { attachComponentDescriptorHandler, registerAllComponentDescriptors } from './Rendering/DomMerging/DomSync';
import { discoverBrowserConfiguration } from './Services/ComponentDescriptorDiscovery';
import { JSEventRegistry } from './Services/JSEventRegistry';
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Web';
import { ConsoleLogger } from './Platform/Logging/Loggers';
import { LogLevel } from './Platform/Logging/Logger';
import { resolveOptions, CircuitStartOptions, ReconnectionOptions } from './Platform/Circuits/CircuitStartOptions';
import { JSInitializer } from './JSInitializers/JSInitializers';
import { enableFocusOnNavigate } from './Rendering/FocusOnNavigate';
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { createBlazorValidation, ensureNovalidateOnForms } from './Validation';

let started = false;
let rootComponentManager: WebRootComponentManager;

function boot(options?: Partial<WebStartOptions>) : Promise<void> {
  if (started) {
    throw new Error('Blazor has already started.');
  }

  started = true;
  options = options || {};
  options.logLevel ??= LogLevel.Error;
  Blazor._internal.isBlazorWeb = true;

  // Defined here to avoid inadvertently imported enhanced navigation
  // related APIs in WebAssembly or Blazor Server contexts.
  Blazor._internal.hotReloadApplied = () => {
    if (hasProgrammaticEnhancedNavigationHandler()) {
      performProgrammaticEnhancedNavigation(location.href, true);
    }
  };

  const jsEventRegistry = JSEventRegistry.create(Blazor);
  rootComponentManager = new WebRootComponentManager(options?.ssr?.circuitInactivityTimeoutMs ?? 2000, jsEventRegistry);

View on GitHub (pinned to 294cab2f9b)

Solutions

  1. Choose either autostart or a single manual Blazor.start(), not both.
  2. Guard the call with a window-level started flag before invoking.
  3. Ensure blazor.web.js is included exactly once per full page load (enhanced navigation does not re-include scripts, but full reloads / iframes might).

Example fix

// before
<script src="blazor.web.js" autostart></script>
<script>Blazor.start();</script> <!-- throws -->

// after
<script src="blazor.web.js"></script>
<script>Blazor.start();</script>
Defensive patterns

Strategy: validation

Validate before calling

function startWebBlazorOnce(opts?: any) {
  if ((window as any).__blazorWebStarted) return Promise.resolve();
  (window as any).__blazorWebStarted = true;
  return Blazor.start(opts);
}

Type guard

function isWebBlazorStarted(): boolean {
  return !!(window as any).__blazorWebStarted;
}

Try / catch

try {
  await Blazor.start(opts);
} catch (e) {
  if (/already started/.test((e as Error).message)) return;
  throw e;
}

Prevention

When it happens

Trigger: Calling Blazor.start() twice on a Blazor Web App page; autostart firing together with a manual start; a JS initializer that calls Blazor.start().

Common situations: Including <script src="blazor.web.js" autostart> plus a manual Blazor.start({...}); a navigation/HMR cycle re-evaluating the bundle; a custom layout that re-includes the script tag.

Related errors


AI-assisted analysis of dotnet/aspnetcore@294cab2f9b (2026-08-06). Data as JSON: /api/errors/0aac84c8dffcef1a. Report an issue: GitHub.