Skip to content

[Validation] IJSObjectReference.DisposeAsync suppresses JSDisconnectedException #69105

Description

@oroztocil

Scenario contact: @dariatiurina

Scenario

This scenario validates that a reported bug is fixed. In Blazor Server apps using Interactive Server render mode, components frequently import JavaScript modules and dispose them in DisposeAsync. When the user closes the browser tab, navigates away, or the circuit otherwise disconnects, the dispose call used to throw JSDisconnectedException:

warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100]
      Unhandled exception rendering component: JavaScript interop calls cannot be issued at this time.
      This is because the circuit has disconnected and is being disposed.
      Microsoft.JSInterop.JSDisconnectedException: JavaScript interop calls cannot be issued at this time...

This forced every component that held an IJSObjectReference to wrap disposal in try/catch, even though the exception conveyed no actionable information: if JavaScript is disconnected, the JS-side object is already gone.

.NET 11 changes JSObjectReference.DisposeAsync to catch and suppress JSDisconnectedException internally. Components can now call await _jsModule.DisposeAsync() without wrapping it in a try-catch, and server logs will no longer contain these warnings during normal teardown.

Minimum build

.NET 11 RC1

Configurations to cover

  • Blazor Web App
    • Static SSR
    • Interactive Server
    • Interactive WebAssembly
    • Interactive Auto
  • Standalone WebAssembly
  • Hybrid (MAUI)

The fix is in the server-side JSInterop runtime. WebAssembly does not have circuits and does not produce this exception.

Also exercise

  • Published output
  • An existing .NET 10 app upgraded to .NET 11
  • Trimming or ahead-of-time compilation
  • More than one server instance, or a proxy in front
  • Hot Reload
  • An IDE as well as the command line
  • Container

Setup

A disconnected circuit is retained for three minutes by default, shorten it, so that the scenario does not appear to pass on both versions:

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents(o =>
    {
        o.DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(5);
    });

What to build

A Blazor Server component that imports a JavaScript module, uses it during its lifetime, and disposes it in DisposeAsync without a try-catch wrapper. The scenario is to disconnect the circuit (close the tab, navigate away) while the component is active and verify that no JSDisconnectedException appears in the server log.

Bracket the disposal with two log statements, one before the call and one after it. An absent exception is a weak signal on its own, because it also looks the same when the component was never disposed at all. With the bracketing, the second line appears only when the call really did complete.

Note that prerendering disposes the component as well, so every run produces two disposal cycles. The first has no circuit involved and completes cleanly on any version. Only the second one is meaningful, so do not read the result off the first pair of log lines.

Things to try

  • Start with .NET 10 and observe the behavior. Then update to .NET 11 and observe again to confirm the fix.
  • Create a component that imports a JavaScript module in OnAfterRenderAsync and stores the reference:
    private IJSObjectReference? _jsModule;
    
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            _jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "./myModule.js");
    }
  • Dispose it without try-catch:
    public async ValueTask DisposeAsync()
    {
        if (_jsModule is not null)
            await _jsModule.DisposeAsync();
    }
  • Run the app, navigate to the component, then close the browser tab or use browser back to an external site.
  • Watch the server console or log output for warnings about JSDisconnectedException.

Expected behavior

Disposing an IJSObjectReference after the circuit has disconnected completes without throwing and does not log a warning. The component cleanup finishes silently.

Must hold

  • The server console shows no JSDisconnectedException warning when the tab is closed while the component is active, and the log statement placed after the disposal call is reached.
  • The component's DisposeAsync does not require a try-catch around the JS module disposal.
  • On .NET 10, the same code produces the JSDisconnectedException warning in the server log.

Evidence to capture

The server log output during circuit disconnection from both versions, showing the bracketing log statements around the disposal call.

What to report

Report results using the format described in the validation testing manual. Include link to a repository with the test app.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ValidationThis issue is used to track validation effortsarea-blazorIncludes: Blazor, Razor Componentsvalidation-scenarioThis issue describes a validation testing scenario

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions