JavaScript interop calls cannot be issued at this time. An interactive component called IJSRuntime from OnInitializedAsync, and that method also runs during prerendering, on the server, before any browser is attached. Move the call into OnAfterRenderAsync behind a firstRender check.

If the result changes what the component shows, call StateHasChanged() once after the call. The other way out is to switch prerendering off for that one component, which has a cost described below.

The error

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.
         at Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime.BeginInvokeJS(JSInvocationInfo& invocationInfo)
         at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, JSCallType callType, CancellationToken cancellationToken, Object[] args)
         at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, JSCallType callType, Object[] args)
         at FixLab.Components.Pages.JsPrerender.OnInitializedAsync() in ...\FixLab\Components\Pages\JsPrerender.razor:line 14

The component that threw reads a value from the browser's local storage:

protected override async Task OnInitializedAsync()
{
    lastPatient = await JS.InvokeAsync<string?>("localStorage.getItem", "lastPatient");
}

Why it happens

With @rendermode InteractiveServer, prerendering is on by default, so the component runs twice. First, during the HTTP request, Blazor renders it to plain HTML so the page arrives with content. Then blazor.web.js opens the circuit and a new instance of the component runs again, live. OnInitializedAsync runs in both passes. In the first there is no browser connection yet, so the JavaScript runtime (RemoteJSRuntime in the trace) refuses the call. The exception escapes the render and the whole response fails with a 500 before any HTML is sent.

OnAfterRenderAsync never runs during prerendering. It runs only in the live pass, after the browser has applied the render, which is exactly when JavaScript can be reached.

The fix

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        lastPatient = await JS.InvokeAsync<string?>("localStorage.getItem", "lastPatient");
        StateHasChanged();
    }
}

Two details matter. The firstRender check keeps it to one call: this method runs after every render, and without the check the StateHasChanged() would trigger another render, which calls this method again. And StateHasChanged() itself is needed because the render has already happened. Without it, local storage held "Maria Garcia" and the page still said "Last patient: none yet"; with it, the page showed "Last patient: Maria Garcia".

The second-best option keeps the call in OnInitializedAsync and turns prerendering off for this component with @rendermode @(new InteractiveServerRenderMode(prerender: false)). That worked too, but the component then sends no HTML at all until the circuit connects: a blank spot on first load, and nothing for search engines to read.

How it was reproduced

A fresh dotnet new blazor -o FixLab --interactivity Server project on .NET SDK 10.0.401 (ASP.NET Core runtime 10.0.12, no extra packages). One page with @rendermode InteractiveServer and @inject IJSRuntime JS called localStorage.getItem in OnInitializedAsync; a plain GET of the page returned 500 with the error above, before any browser was involved. The fixed page was then opened in a browser with a value stored under lastPatient, and showed it.

Frequently asked

Why can't I call JavaScript in OnInitializedAsync in Blazor?
With prerendering on, OnInitializedAsync first runs on the server while the page HTML is being generated, before any browser is connected, so there is nothing to call. OnAfterRenderAsync runs only once the component is live in the browser.
Do I need StateHasChanged after JS interop in OnAfterRenderAsync?
Yes, when the result changes what the component shows. The render has already happened by the time OnAfterRenderAsync runs, so call StateHasChanged once, inside the firstRender check, to display the new value.
How do I disable prerendering for one Blazor component?
Give it the render mode new InteractiveServerRenderMode(prerender: false) instead of InteractiveServer. JavaScript interop in OnInitializedAsync then works, but the component renders nothing until the circuit connects, and crawlers see no content for it.

More decoded errors in the Fixes category. What runs when, and why, is in the component lifecycle part of the Blazor series.