Telerik blogs
DotNetT2 Light_1200x303

Errors are likely to occur, the question is how to handle them. Let’s take a look at best practices in handling them in your Blazor app.

Sooner or later, something is bound to go wrong in your Blazor app.

Be it an unexpected input, an edge case you didn’t preempt, or your web host taking your DB down for maintenance. Errors are likely to occur, the question is how to handle them.

There are a number of factors here, including:

  • How best to handle errors, so users aren’t left staring at a broken screen
  • How to diagnose and fix the problem when something does go wrong
  • Tactics you can use to make sure you’re aware of problems in production
  • Steps you can take to avoid or handle predictable errors

If you’re building Blazor web applications, you have a few options for tackling these challenges. Let’s take a look.

Errors During Development

The first place to catch errors is when you’re building your Blazor app. You’ll see slightly different errors depending on which hosting model you’re using (Server or WASM).

Blazor Server Errors During Development

If your Blazor Server app runs into a problem during development, you’re likely to see a message directing you to the browser console for more details.

Browser window showing default Blazor Server error text warning about an un unhandled exception

The console will show you some more details, but if you really want to dig into the problem you’ll want to head over to your server to see more detailed errors.

If you’re running locally using the built-in ASP.NET web server, you’ve most likely got a terminal window open somewhere that looks a bit like this:

Terminal window with text outlining the unhandled exception details including stack trace

If you’re using Visual Studio you’ll also find this information reported in the Output > Debug window.

It’s fair to say there’s quite a bit of noise in this window! But if you scroll up to the first reported error you’ll usually find some details which can help track down the problem.

In this case its reporting a System Exception thrown on line 48 of FetchData.razor.

System.Exception: Oof, an error occurred
   at BestPractices.BlazorServer.Pages.FetchData.OnInitializedAsync() in C:\Users\hilto\RiderProjects\Telerik\BestPractices\BestPractices.BlazorServer\Pages\FetchData.razor:line 48
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost: Error: Unhandled exception in circuit '9s8O3FNORDmRpW-IDAWcj_20c8obFpFxAaMBzPaH-xg'.

Sure enough, there’s our problem.

FetchData.razor

...

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        throw new Exception("Oof, an error occurred");        
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

Blazor WASM Errors During Development

You’ll see something similar in your Blazor WASM app, albeit with slightly different wording.

An unhandled error has occurred. Reload

Because the Blazor components are running in the browser you’ll automatically see more detailed/directly useful errors in the browser console.

Browser window showing default Blazor WASM error text warning about an un unhandled exception

In this case, the error is to be found at Index.razor, line 15.

Keeping the Lights On, Using Error Boundaries

If your Blazor Server app throws an error (see above), it effectively blocks your users from continuing, until they reload the app.

Blazor WASM is a little more forgiving and will let you dismiss the error and carry on.

Either way, though, you probably want to provide some more friendly and/or helpful information to your users when something goes wrong.

One way to achieve this is via error boundaries. You can wrap a part of your app in an error boundary, providing a handy way to catch and handle exceptions without bringing your entire application to its knees.

Take this example, where we have a page which renders a ProductDetails component, plus the standard Counter component.

@page "/"

<ProductDetails />
<Counter />

Here’s how that ProductDetails component looks:

<h3>ProductDetails</h3>

<button @onclick="BreakMe">Break me!</button>

@code {

    private void BreakMe()
    {
        throw new Exception("I'm broken!");
    }

}

If we run this and click the button we’ll get that standard error bar we saw earlier and, in the case of Blazor Server, our app will end up in a “broken” state, requiring a reload to get back up and running.

To make this experience better for our users we can wrap the ProductDetails component in an ErrorBoundary.

<ErrorBoundary>
	<ProductDetails />
</ErrorBoundary>

With this in place, when we click the button, the exception still occurs but is handled, and the default UI for an error boundary is shown (in the same place as the component which has thrown the error).

Blazor component with a visible warning on part of the screen that

The good news is this keeps our app operational, and limits the exception to that one part of the UI. In this example we can still navigate to other pages and/or interact with the counter widget on this page, even in Blazor Server (where this would usually have put the circuit into a broken state and blocked the user from continuing).

By default, the ErrorBoundary component will render a div with the blazor-error-boundary CSS class when an error occurs.

You can override this and provide your own custom UI by defining an ErrorContent property.

<ErrorBoundary>
    <ChildContent>
        <ProductDetails/>
    </ChildContent>
    <ErrorContent>
        <div class="border p-4 text-danger">
            Sorry about that, something seems to have gone awry.
        </div>
    </ErrorContent>
</ErrorBoundary>

If you want to dig further into error boundaries, check out this helpful post on Working With Unhandled Exceptions by Dave Brock.

Anticipate and Handle Errors

Everything we’ve talked about so far is in the context of unhandled exceptions.

If you choose to handle an exception, you can of course apply whichever logic you deem fit (the user need never know!). So one strategy is to anticipate and handle errors using a standard C# try/catch approach.

For example, in our ProductDetails example, we could try to fetch product details and handle any errors that might occur.

<h3>ProductDetails</h3>

@if (loadFailed)
{
    <h2>Something went wrong fetching product details</h2>
}

<button @onclick="BreakMe">Break me!</button>

@code {

    bool loadFailed = false;

    private void BreakMe()
    {
        try
        {
            loadFailed = false;
            // load product details here
            throw new Exception("Ooof");
        }
        catch (Exception e)
        {
            loadFailed = true;
        }
    }
}

In practice, you probably don’t want to wrap everything in try/catch blocks and you can always use error boundaries to catch the errors instead.

But, if you have specific exceptions you want to handle, then the humble try/catch is a good option to have.

Retry Network Requests

We tend to assume that networks are reliable, and that a call to a backend API will always work, but in practice transient issues can occur when interacting with servers, and servers can become temporarily unavailable (especially when deployments are being pushed out, or tweaks made to the network which hosts the server).

For these reasons, it pays to assume that your network calls will sometimes fail, especially if you’re building a Blazor WASM app where most of your backend logic lives in a backend Web API.

You could take the brute force approach of manually wrapping every backend call in a try/catch, but this adds a lot of noise to your application and still leaves you facing the question of what to do when a network call does fail.

Instead, for these kinds of transient errors I prefer to use Polly in my Blazor apps.

Polly is an open-source library for defining retry policies and it works nicely with HttpClient to handle transient network failures.

First we need to add a reference to the NuGet package:

Install-Package Microsoft.Extensions.Http.Polly -Version 6.0.7

Then, in our Blazor WASM app we need to tweak Program.cs to use IHttpClientBuilder to register a HttpClient, and specify a retry policy for failed requests.

Program.cs

...

builder.Services
    .AddHttpClient("Default",client => client.BaseAddress =  new Uri(builder.HostEnvironment.BaseAddress))
    .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5)
    }));

In this case, should an exception occur when we make a HTTP call, Polly will retry the call twice, once after 1 second, and again after 5. At that point, if the backend call is still throwing exceptions, the exception will be thrown as normal and our app will react accordingly.

Finally we just need to tell our WASM app to use this Default HttpClient when it requests an instance of HttpClient.

builder.Services.AddScoped(sp => sp.GetService<IHttpClientFactory>().CreateClient("Default"));

This is to maintain backwards compatibility. The WASM app will now use our “Default” HttpClient, complete with retry policy, whenever we inject HttpClient into a component.

Diagnose and Fix Errors in Production

When errors occur in production, they can be tricky to pin down; the key is to have enough information to try and understand/fix the problem.

This is where ASP.NET’s built-in logging mechanisms are really useful.

When you run your app (in development or production), you’ve probably noticed all those messages showing up in the console (like the one we explored earlier when we encountered an exception during development).

ASP.NET relies on something called ILogger to log information, warnings and errors to the console by default.

We can also lean on that in production to get detailed error logs when something goes wrong.

The implementation details vary here, depending on your hosting infrastructure and you can choose to store logs in a number of places.

The default logging destinations for Blazor Server are:

  • Console
  • Debug
  • EventSource
  • EventLog (in WIndows)

Blazor WASM, as it runs in the users browser, doesn’t have access to the client’s file system, or network.

Therefore, if you’re logging from Blazor WASM, you’re more limited in terms of where you can send logs, but your app is also likely to be interacting with a backend API which can record its own logs.

With both Server and WASM you can send logs to other destinations via third-party libraries (typically installed via NuGet), such as Elmah, Sentry and Serilog, to name a few.

Alternatively, you can go further and plug your app into a third-party Application Performance Management service, such as Azure Application Insights and RayGun, which will often give you much more detailed information about exactly what went wrong, and what else was happening at the time.

However you choose to store them, you’ll want to consider what “level” of log to store.

If you take a look at your appsettings.json file in your Blazor Server app (or backend Web API if you’re using Blazor WASM), you’ll likely see this configuration:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

This indicates that, in production, the default logging behavior is to record logs at the level of “Information” (and above), except for errors from ASP.NET Core which will only be logged if they are at “Warning” level or above.

If you’re using Blazor WASM you can control this level via Program.cs.

builder.Logging.SetMinimumLevel(LogLevel.Warning);

As well as capturing existing logs, you might want to log additional information from your components. You can do this by injecting an instance of ILogger, then invoking its LogX methods (where X is the level).

ProductDetails.razor

@inject ILogger<ProductDetails> Logger

<h3>ProductDetails</h3>
<button @onclick="BreakMe">Break me!</button>
@code {   

    private void BreakMe()
    {
        try
        {            
            // load product details here           
        }
        catch (Exception e)
        {            
            Logger.LogError("Something went wrong retrieving product details");
        }
    }
}

Alerts When Something Goes Wrong

Finally, if you do choose to lean on a third-party library for error logging, you may find they can also raise notifications (for example, via email) when something goes wrong.

If your app is broken, you probably want to know about it! Alert emails are a useful option to make sure you know something’s up.

Tools like Elmah and Serilog, and APMs like RayGun and Application Insights can handle this for you, and also make sure you don’t get flooded with emails when the same error starts recurring more than once.

In Conclusion

Errors are an inevitable part of web development. Blazor, and ASP.NET Core in general, provides several mechanisms to help you handle them.

Error boundaries can contain unhandled exceptions and keep your Blazor Server app running even though one component has failed.

Whether you’re using Blazor WASM or Server, knowing what’s gone wrong in Production or Development is key; use logs to make sure you can dig into the details of any errors which occur.

Consider third-party tools to surface logs and send alert emails when problems occur, and implement a simple retry policy for network requests (so your app can withstand transient network issues).

More Blazor Best Practices: Loading Data

Jon Hilton
About the Author

Jon Hilton

Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.

Related Posts

Comments

Comments are disabled in preview mode.