Harald Coppoolse
Harald Coppoolse

Reputation: 30474

Async-await: Does the thread run until my await?

I always thought that if I call an async function, the thread starts executing this async function until it sees an await. Instead of waiting idly I thought that it would go up the call stack to see if the caller is not awaiting. If not, it executes the code.

Consider the following (simplified) code:

async Task<string> FetchCustomerNameAsync(int customerId)
{
    // check if customerId is positive:
    if (customerId <= 0) throw new ArgumentOutofRangeException(nameof(customerId);

    // fetch the Customer and return the name:
    Customer customer = await FetchCustomerAsync(customerId);
    return customer.Name;
}

Now what happens if my async function would call FetchCustomerNameAsync(+1) without awaiting:

var myTask = FetchCustmerNameAsync(+1);
DoSomethingElse();
string customerName = await myTask;

What I thought, is that before the await in my function is met, the check on the parameter value already has been done.

Therefore the following should lead to an exception before the await:

// call with invalid parameter; do not await
var myTask = FetchCustmerNameAsync(-1);      // <-- note the minus 1!
Debug.Assert(false, "Exception expected");

I'd think that although I was not awaiting, that the check on parameter value was performed before the Debug.Assert.

Yet, in my program no exception is thrown before the Debug.Assert Why? What is really happening?

Addition after comments

Apparently some people didn't want the simplified code, but my original test code. Although I don't think it will help describing the problem, here it is.

Microsoft about usage of local functions in C# 7.
This article describes that an exception will not be detected before the await (as my question was). This astounded me, because I always thought that the parameter was already checked. So I wrote some test code. (now I know better, thanks to answerers and commentors).

So here is my Non-simplified test code. It compiles, it runs, and it shows the effect. However it does not help describing the question, it only distracts from it. But for those who are still interested after all these warnings:

async Task DemoLocalFunctionsInAwaitAsync()
    {
        // using local functions after parameterchecks gives errors immediately

        // No exception before await:
        Task<int> task1 = OldMethodWithoutLocalFunction(null);
        // See? no exception

        // New method: exception even if no await
        try
        {
            Task<int> task2 = NewMethodUsingLocalFunction(null);
            // no await, yet an exception
            Debug.Assert(false, "expected exception");
        }
        catch (ArgumentNullException)
        {
            // this exception is expected
        }

        try
        {
            // await the first task that did not throw an exception: expect the exception
            await task1;
            Debug.Assert(false, "expected exception");
        }
        catch (ArgumentNullException)
        {
            // this exception is expected
        }
    }

Below the function as I would normally write it:

    async Task<int> OldMethodWithoutLocalFunction(Customer c)
    {
        // this does not throw exception before awaited
        if (c == null) throw new ArgumentNullException(nameof(c));
        await Task.CompletedTask;
        return c.CustomerId;
    }

This is the function that uses the local function. Almost as described in the Microsoft article mentioned above.

    async Task<int> NewMethodUsingLocalFunction(Customer c)
    {
        // this method gives an exception even if not awaited yet
        if (c == null) throw new ArgumentNullException(nameof(c));
        return await LocalFetchCustomerIdAsync(c);

        async Task<int> LocalFetchCustomerIdAsync(Customer customer)
        {
            await Task.CompletedTask;
            return customer.CustomerId;
        }
    }

If you look closely: this will also not help (and I understand now why, thanks to answerers and commentors).

Upvotes: 0

Views: 197

Answers (3)

FCin
FCin

Reputation: 3915

You are right about that thread executes async function until it sees an await. In fact your ArgumentOutofRangeException is thrown by the thread on which you call FetchCustmerNameAsync. The reason why you don't get exception even though it is the same thread is because when you use await inside a function, a AsyncStateMachine is built. It converts all code into state machine, but the important part is how it handles exception. Take a look:

This code:

public void M() {

    var t = DoWork(1);

}

public async Task DoWork(int amount)
{
    if(amount == 1)
        throw new ArgumentException();

    await Task.Delay(1);
}

Gets converted into (I skipped unimportant parts):

private void MoveNext()
{
    int num = <>1__state;
    try
    {
        TaskAwaiter awaiter;
        if (num != 0)
        {
            if (amount == 1)
            {
                throw new ArgumentException();
            }
            awaiter = Task.Delay(1).GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                // Unimportant
            }
        }
        else
        {
            // Unimportant
        }
    }
    catch (Exception exception)
    {
        <>1__state = -2;
        <>t__builder.SetException(exception); // Add exception to the task.
        return;
    }
    <>1__state = -2;
    <>t__builder.SetResult();
}

If you follow <>t__builder.SetException(exception); (AsyncMethodBuilder.SetException), you will find that it eventually calls task.TrySetException(exception); which adds the exception to the task's exceptionHolder, which can be retrieved with Task.Exception property.

Upvotes: 1

Oscar Vicente Perez
Oscar Vicente Perez

Reputation: 857

Exceptions only propagates when a task is awaited

You can't handle an exception without awaiting the Task. The Exception(s) only propagate within a Thread/Task. So if you don't await, the Exception just stops the Task. And if the exception is thrown before you await, it will propagate when you actually await.

Do all the validations before, and then do the asynchronous work.

So, I suggest you to validate before:

ValidateId(id); // This will throw synchronously.
Task<Customer> customer = FetchCustomerAsync(id).ConfigureAwait(false);
DoSomethingElse();
return await customer.Name;

This is the best way to achieve the parallelism you want.

Upvotes: 2

bommelding
bommelding

Reputation: 3037

A simplified MCVE :

    static async Task Main(string[] args)
    {       
        try
        {
          // enable 1 of these calls
            var task = DoSomethingAsync();
          //  var task = DoSomethingTask();

            Console.WriteLine("Still Ok");
            await task;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);                
        }
    }

    private static async Task DoSomethingAsync()
    {
        throw new NotImplementedException();            
    }

    private static Task DoSomethingTask()
    {
        throw new NotImplementedException();
        return Task.CompletedTask;
    }

When you call DoSomethingAsync, you will see the "Still Ok" message.

When you call DoSomethingTask, you will get the behaviour you expect: an immediate exception before the WriteLine.

Upvotes: -1

Related Questions