Timeout exceptions with async/await in .NET4.5 MVC4 with C#

This post will discuss timeouts that occur with await and async in .NET4.5. For clarity on async and await in MVC4 check out my previous two blog posts: Await and async in .NET4.5 and Async controllers and actions in .NET4.5 MVC4

As await operations may involve some seriously long running actions, such as calling a slow web service, it can be a good idea to specify a timeout. We may not want to make the visitor wait 60 seconds just to see an error message afterwards. If your experience tells you that a web service normally responds within 5 seconds at most then it may be pointless waiting 50-60 seconds as you can be sure something has gone wrong. ASP.NET has a default request timeout of 90 seconds – correct me here if I’m wrong – but we can specify other values directly in code with an attribute: AsyncTimeout that takes the timeout value in milliseconds as parameter.

In addition to the AsyncTimeout attribute you’ll also need to supply an additional parameter of type CancellationToken to the async action. This parameter can be used by the long running services to check if the user has requested a cancellation. The CancellationToken has an IsCancellationRequested property which provides exactly this type of information. In our example we’ll pass this token to the service calls and use it to throw an exception if the request has been cancelled. As our services are not real service calls, there is no clean-up work to do but imagine that if an IO operation is interrupted by a user then the cancellation token can throw an exception and you can clean up all open resources or roll back the database operations in a catch clause.

You can read more about cancellation tokens on MSDN: Cancellation token on MSDN

Update service methods:

public async Task<String> GetDataAsync(CancellationToken ctk)
        {
            StringBuilder dataBuilder = new StringBuilder();
            dataBuilder.Append("Starting GetData on thread id ").Append(Thread.CurrentThread.ManagedThreadId)
                .Append(". ");
            ctk.ThrowIfCancellationRequested();
            await Task.Delay(2000);
            dataBuilder.Append("Results from the database. ").Append(Environment.NewLine);
            dataBuilder.Append("Finishing GetData on thread id ").Append(Thread.CurrentThread.ManagedThreadId)
                .Append(".");
            return dataBuilder.ToString();
        }
public async Task<String> GetResultAsync(CancellationToken ctk)
        {
            StringBuilder resultBuilder = new StringBuilder();
            resultBuilder.Append("Starting GetResult on thread id ").Append(Thread.CurrentThread.ManagedThreadId)
                .Append(". ");
            ctk.ThrowIfCancellationRequested();
            await Task.Delay(2000);
            resultBuilder.Append("This is the result of a long running calculation. ");
            resultBuilder.Append("Finishing GetResult on thread id ").Append(Thread.CurrentThread.ManagedThreadId)
                .Append(".");
            return resultBuilder.ToString();
        }

We know that the Index() action needs about 2 seconds to complete so let’s try something more aggressive to see what happens:

[AsyncTimeout(1000)]
        public async Task<ActionResult> Index(CancellationToken ctk)
        {
            DateTime startDate = DateTime.UtcNow;

            HomePageViewModel viewModel = new HomePageViewModel();
            viewModel.AddMessage(string.Concat("Starting Action on thread id ", Thread.CurrentThread.ManagedThreadId));
            CalculationService calcService = new CalculationService();
            DatabaseService dataService = new DatabaseService();

            Task<String> calculationResultTask = calcService.GetResultAsync(ctk);
            Task<String> databaseResultTask = dataService.GetDataAsync(ctk);

            await Task.WhenAll(calculationResultTask, databaseResultTask);

            viewModel.AddMessage(calculationResultTask.Result);
            viewModel.AddMessage(databaseResultTask.Result);

            DateTime endDate = DateTime.UtcNow;
            TimeSpan diff = endDate - startDate;

            viewModel.AddMessage(string.Concat("Finishing Action on thread id ", Thread.CurrentThread.ManagedThreadId));
            viewModel.AddMessage(string.Concat("Action processing time: ", diff.TotalSeconds));
            return View(viewModel);
        }

It is no surprise that we get a timout exception upon running the application:

Timeout YSOD

The yellow screen of death is great for debugging but not so nice in a production environment. To turn on custom error messages you must change web.config: locate the customErrors tag under system.web and change the mode attribute to “On” for the production environment. If your web.config does not have this tag then add it:

<system.web>
    <customErrors mode="On"></customErrors>

There is a default view in the Shared folder within Views called Error.cshtml. After modifying the web.config file the user will be redirected to that view upon an unhandled exception:

Error.cshtml screen

You can of course create custom views for errors and then specify which error view to show using attributes. Example:

[AsyncTimeout(1000)]
        [HandleError(ExceptionType = typeof(TimeoutException), View = "Timeout")]
        public async Task<ActionResult> Index(CancellationToken ctk)

This way you can specify error views for specific types of unhandled exceptions.

The next post will look at exception handling in async methods.

View the list of MVC and Web API related posts here.

ultimatemindsettoday

A great WordPress.com site

Elliot Balynn's Blog

A directory of wonderful thoughts

Robin Sedlaczek's Blog

Developer on Microsoft Technologies

HarsH ReaLiTy

A Good Blog is Hard to Find

Softwarearchitektur in der Praxis

Wissenswertes zu Webentwicklung, Domain-Driven Design und Microservices

the software architecture

thoughts, ideas, diagrams,enterprise code, design pattern , solution designs

Technology Talks

on Microsoft technologies, Web, Android and others

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

Anything around ASP.NET MVC,WEB API, WCF, Entity Framework & AngularJS

Cyber Matters

Bite-size insight on Cyber Security for the not too technical.

Guru N Guns's

OneSolution To dOTnET.

Johnny Zraiby

Measuring programming progress by lines of code is like measuring aircraft building progress by weight.

%d bloggers like this: