Nothing is worse than waiting around for an unresponsive UI, am I right? With frontend frameworks, there’s a real possibility that we’ll leave our using waiting for responses that never return. Blazor isn’t any different in this regard and can cause the same frustrations in our users.

This post will show how we can set reasonable timeouts for dependency calls and give our users back control and cancel an existing asynchronous operation.

CancellationTokenSource and Timeouts

It should go without saying, but all software has a failure in its future, and no operation is guaranteed to succeed. When we perform calls over the network, we should understand what’s a good time to wait for a successful response from our dependency, and forever is never a good answer.

In a Blazor application, we’ll be calling several asynchronous operations, and we need to account for slow or unresponsive services. Luckily, most (good) async APIs allow us to pass in a CancellationToken, which will enable us to cancel the call maybe. I say maybe because not all asynchronous calls are cancellable, but we still may be able to abort waiting for a response.

Let’s look at how we can create an instance of CancellationToken tied to a timeout of 3 seconds. In our example, we’ll be calling a weather forecast API, which may timeout from time to time. Inside our Blazor component’s @code block, we’ll find our GetForecast method.

 private async void GetForecast(EditContext editContext)
 {
     var source = new CancellationTokenSource(TimeSpan.FromSeconds(3));
     Message = null;
     
     try
     {
         _loading = true;
         _forecast = await _client.Forecast
             .GetForecastAsync(new ForecastRequestEntity()
             .WithPostalCode(Input.Value)
             .WithDays(3), source.Token
         );
     }
     catch (Exception e)
     {
         Message = source.IsCancellationRequested 
         ? "Request to API timed out" 
         : e.Message;
         
         _logger.LogError(e, "couldn't retrieve forecast");
     }
     finally
     {
         _loading = null;
         // poke blazor to reset 
         // in case an error has occurred
         StateHasChanged();
     }
 }

Let’s walk through the code:

  1. We create a CancellationTokenSource and pass in a TimeSpan instance of 3 seconds.
  2. When calling our client’s GetForecastAsync method, we pass our CancellationToken using source.Token.
  3. If the request times out, our code will throw an OperationCanceledException which we can handle in the code after our async call.

Note, the phrase CancellationToken is misleading. It only keeps a Task from starting but does not cancel an already started Task. The returning response from the Task may cause your UI to update if it does succeed. To keep the UI from updating after we’ve canceled, we need to check the IsCancellationRequest property on the source.

var result = await _client.Forecast
    .GetForecastAsync(new ForecastRequestEntity()
        .WithPostalCode(Input.Value)
        .WithDays(3), source.Token
    );

if (source?.IsCancellationRequested == false) {
    _forecast = result;    
}

We can even give users control of canceling the Task by setting the CancellationTokenSource to a member of our Blazor component.

<button class="btn btn-primary" type="submit" @onclick="() => source?.Cancel()">
    <span class="spinner-grow spinner-grow-sm" role="status" aria-hidden="true"></span>
    Loading...
</button>

Let’s change our method to reflect the change.

private async void GetForecast(EditContext editContext)
{
    // set the member, not a local variable
    source = new CancellationTokenSource(TimeSpan.FromSeconds(3));
    Message = null;

    try
    {
        _loading = true;
        var result = await _client.Forecast
            .GetForecastAsync(new ForecastRequestEntity()
                .WithPostalCode(Input.Value)
                .WithDays(3), source.Token
            );
        
        if (source?.IsCancellationRequested == false) {
            _forecast = result;    
        }
    }
    catch (Exception e)
    {
        Message = source?.IsCancellationRequested == true
            ? "Request to API timed out" 
            : e.Message;

        _logger.LogError(e, "couldn't retrieve forecast");
    }
    finally
    {
        _loading = null;
        source = null;
        // poke blazor to reset 
        // in case an error has occurred
        StateHasChanged();
    }
}

Conclusion

We saw two ways where we can give users back a responsive Blazor UI. First, we can set timeouts with CancellationTokenSource, and secondly, we can manually cancel by invoking the Cancel method. It is important to note, Tasks may not stop executing when canceled, which means it’s essential for us to determine whether we want to set our variables by checking the IsCancellationRequested property.

To see this example in its entirety, check out the GitHub Repository with a working sample. You’ll need access to an API token, but its free and easy to sign up for one.

I hope you enjoyed this post, and I wish you success on your Blazor journey.