Kun Hu
Kun Hu

Reputation: 427

Would 'finally' block run once when there are multiple Tasks in the try-catch?

I have below C# code that runs periodically, e.g., every 1 hour. Every time it runs, it tries to make some remote calls in parallel. When any of these remote calls throws an exception, I create only one ticket, or update the previous ticket. And in the finally block, resolve the existing previous ticket if there is no exception this time. Essentially I want to make sure that no matter how many remote calls fail, I only have one ticket to investigate. And when next time all calls succeed, the ticket gets resolved automatically.

However, when the remote calls all succeed in the try block, the call in finally block tries to resolve the ticket, but ran into HTTP 412 Precondition Failed, meaning that the ticket I got in try block was somehow updated before the finally block. If it's updated in the same thread, I wouldn't try to resolve it.

I learnt from this post that Task.WhenAll will wait for all tasks to complete even in the presence of failures (faulted or canceled tasks). In case where multiple tasks throw exceptions, would the catch block run once or more? How about the finally block?

Ticket ticket = null;
try
{
    // Query to get the existing ticket if there is any.
    ticket = await QueryExistingTicketAsync();

    // Make a lot of remote calls in parallel
    var myTasks = new List<Task>();
    myTasks.Add(RemoteCallAsync("call1"));
    myTasks.Add(RemoteCallAsync("call2"));
    myTasks.Add(RemoteCallAsync("call3"));
    // ... add more Tasks for RemoteCallAsync()

    await Task.WhenAll(myTasks);
}
catch (Exception ex)
{
    if (ticket != null)
    {
        ticket.ReChecked = true;
        ticket.LastCheckTime = DateTimeOffset.Now;

        // If the previous ticket exists, meaning the last run failed as well, 
        // update the timestamp on that ticket.
        ticket = await UpdateExistingTicketAsync(ticket);
    }
    else
    {
        // If the previous ticket does not exist yet, 
        // create one ticket for investigation, this ticket.ReChecked will be true
        ticket = await CreateNewTicketAsync();
    }
}
finally
{
    // Resolve the previous ticket if it was not created/updated in catch block
    if (ticket != null && !ticket.ReChecked)
    {
        ticket.Status = "Resolved";
        await UpdateExistingTicketAsync(ticket);
    }
}

Upvotes: 0

Views: 1208

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43525

The catch and the finally blocks are running once, but can run both. The finally block is always running, while the catch block runs only in case of an exception, before the finally block.

There is another issue with your code though.

try
{
    Ticket ticket = await QueryExistingTicketAsync();
    // ...
}
catch (Exception ex)
{
    if (ticket != null)
    {

The ticket you create on try and the ticket you update on catch is not the same ticket. The ticket declared on try has local scope, and so it's not visible outside this block. You may have declared another ticket variable before the try-catch-finally block, but even in this case the compiler would complain for the double variable declaration. So in any case the code you provided should not be able to compile.

Upvotes: 1

V0ldek
V0ldek

Reputation: 10563

Short answer: even if multiple tasks throw exceptions, there will be only one exception thrown by await Task.WaitAll and thus the catch block will execute only once. The finally block always executes once per entry to the try block. Long answer follows.

Awaiting a Task.WaitAll, if any wrapped Tasks throw an exception, throws an AggregateException containing exceptions from all of the Tasks. So assuming you've reached the await Task.WhenAll(tasks) line without exceptions, here's what happens:

  1. Task.WhenAll will wait for all the Tasks to complete, regardless of whether they fault or not.
  2. If all of the Tasks completed successfully, the Task.WhenAll completes successfully, execution resumes at the await, end of the try block is reached, the finally block executes.
  3. If one or more Tasks finished with an exception, Task.WhenAll wraps all of them into an AggregateException and completes in a faulted state. The execution resumes at the await and the aforementioned AggregateException is thrown. The catch block is executed and then the finally block.

So, assuming that one thread enters the try block, in both cases when the execution resumes at the await only one thread continues the execution. The only way for you to enter the catch or finally blocks twice is to execute the method twice.

The await statement does a lot of magic under the hood, but it never causes your execution to continue on multiple threads. One thread awaits, only one thread picks it up and continues.

Upvotes: 2

Related Questions