Blazor WebAssembly, JavaScript Interop, and Safari: Why I Avoid async for Navigation

When working with Blazor WebAssembly, JavaScript interop often feels straightforward—until it isn’t. One of the more subtle (and frustrating) issues I’ve run into involves Safari, user-initiated navigation, and async event handlers.

This post documents a real-world lesson learned: why I intentionally avoid async when triggering browser navigation (like mailto:) from Blazor, even though async/await is usually considered best practice.


The Scenario

I had a simple requirement:

  • A MudBlazor button
  • Track a telemetry event (Application Insights)
  • Open the user’s email client using mailto:

The Blazor component looked roughly like this:

<MudButton OnClick="OnClick">
    Contact Us
</MudButton>

And the click handler:

private void OnClick(MouseEventArgs _)
{
    Telemetry.TrackEmailClick(Source, Page ?? "unknown");
    JS.InvokeVoidAsync("open", "mailto:sales@tanolis.us", "_self");
}

No await. No async.

This worked consistently across Chrome, Edge, and Firefox—and, crucially, Safari.


The Temptation: “Make It Async”

From a .NET perspective, the “correct” version might look like this:

private async Task OnClick(MouseEventArgs _)
{
    Telemetry.TrackEmailClick(Source, Page ?? "unknown");
    await JS.InvokeVoidAsync("open", "mailto:sales@tanolis.us", "_self");
}

This compiles. It looks clean. It follows modern C# guidance.

And on Safari… it starts failing.


What Goes Wrong on Safari

Safari (especially on iOS and embedded WebViews) has long-standing quirks with:

  • async event handlers
  • Microtask scheduling
  • JavaScript → native navigation handoffs
  • WASM + browser event loop interaction

The failure mode is subtle:

  1. User clicks the button
  2. Telemetry call runs
  3. await yields control
  4. Browser decides the click is no longer a “direct user gesture”
  5. Navigation (mailto: / window.open) is silently ignored

No exception. No console error. Just… nothing happens.

This is not theoretical—it’s reproducible and widely encountered in real apps.


The Key Rule I Now Follow

In Blazor + Safari, never await before triggering browser navigation.

That includes:

  • await JS.InvokeVoidAsync(...)
  • await fetch(...)
  • Any Promise-based wrapper
  • Even await Task.Yield()

If navigation depends on the click being considered a user gesture, you must not yield execution.


Why Fire-and-Forget Is Correct Here

In this scenario:

  • I don’t need a return value
  • I don’t need to handle JS errors
  • The action is terminal (navigation)
  • Telemetry is best-effort

So fire-and-forget is not a shortcut—it’s the correct design.

private void OnClick(MouseEventArgs _)
{
    Telemetry.TrackEmailClick(Source, Page ?? "unknown");
    JS.InvokeVoidAsync("open", "mailto:sales@tanolis.us", "_self");
}

This keeps:

  • The handler synchronous
  • Safari happy
  • User behavior consistent across browsers

A Small Safari-Friendly Improvement

Safari is more reliable with location.href than window.open.

JavaScript helper:

window.openMail = (email) => {
    window.location.href = `mailto:${email}`;
};

Blazor call:

JS.InvokeVoidAsync("openMail", "sales@tanolis.us");

Still synchronous. Still safe.


What I Avoid on Purpose

These patterns have caused real issues for me on Safari:

  • async void OnClick(...)
  • await JS.InvokeVoidAsync(...) before navigation
  • Promise chains in click handlers
  • Telemetry calls after navigation
  • Over-engineering interop for simple user actions

Sometimes the simplest code is the most correct.


The Bigger Lesson

“Best practice” depends on context.

In a browser + WASM + JS interop + Safari environment, purity gives way to pragmatism. Understanding how browsers interpret user gestures matters more than following async patterns blindly.

This is one of those cases where experience beats theory.


Final Takeaway

If you’re triggering navigation from Blazor WebAssembly:

  • Keep the event handler synchronous
  • Fire telemetry first
  • Trigger navigation immediately
  • Do not await
  • Especially if Safari matters

Your future self (and your users) will thank you.

FavoriteLoadingAdd to favorites

Comments

Leave a Reply


RECENT POSTS


Categories



Tags

ADO ai angular asian asp.net asp.net core azure ACA azure administration Azure Cloud Architect Azure Key Vault Azure Storage Blazor WebAssembly BLOB bootstrap Branch and Release flow c# c#; ef core css datatables design pattern docker excel framework Git HTML JavaScript jQuery json knockout lab LINQ linux powershell REST API smart home SQL Agent SQL server SSIS SSL SVG Icon typescript visual studio Web API window os wordpress


ARCHIVE


DISCLAIMER