Does an exception propagate using ConfigureAwait(false) with async Task and await?

641 views Asked by At

I was feeling like I had quite a good grip on async await programming, but what happened today made me perplexed. I have been searching for an answer to this for a little while now, but was not able to find it anywhere. I have read quite a bit about async await programming, but my feeble mind is not capable of understanding what exactly is happening in this particular scenario.

I have these two methods:

public async Task<IEnumerable<ServerStatus>> GetServersStatusAsync()
{
    var serverStatuses = new List<ServerStatus>();
    try
    {
        ServerStatusRequest request = new ServerStatusRequest();
        var serverStatusResponse = await GetAsync<ServerStatusResponse>(request).ConfigureAwait(false);
    }
    // I would expect the exception thrown from GetAsync to be caught here. But it doesn't always do that.
    catch (Exception ex)
    {
        _log.Error("Failed to retrieve server statuses.", ex);
    }
    return serverStatuses;
}

And

private async Task<T> GetAsync<T>(IRequest request)
{
    string respString = string.Empty;
    try
    {
        
        var requestUrl = request.GetQueryString(_apiToken);
        _log.Debug($"Sending a request to {requestUrl}");
        CancellationTokenSource tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutInSec));

        //This call can throw an exception. It will always get caught inside this current try catch, 
        //but if after the await we are continuing on a different thread, 
        //the re-thrown/propagated exception is not caught by the calling method.
        var response = await _client.GetAsync(requestUrl, tokenSource.Token).ConfigureAwait(false);

        if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.BadRequest)
        {
            respString = await response.Content.ReadAsStringAsync();
            var deserializedObject = JsonConvert.DeserializeObject<T>(respString);
            return deserializedObject;
        }
    }
    catch (Exception ex) when (ex is JsonReaderException || ex is JsonSerializationException)
    {
        throw new JsonSerializationException($"Failed to deserialize {respString}", ex) { Source = respString };
    }

    return default(T);
}

I have added 2 comments to the code snippets as pointers, but the basic idea is:

In GetServerStatusAsync I have wrapped everything with a try catch as I want to handle the exceptions there. I call GetAsync which awaits a call to HttpClient.GetAsync with ConfigureAwait(false). When the call from HttpClient.GetAsync returns with an exception if we are no longer on the initial thread/context, it can be caught inside my GetAsync method, but will not get propagated to GetServerStatusAsync. If I remove ConfigureAwait(false), it always propagates down as I would expect, but with ConfigureAwait(false) it's more of a 50/50.

Is there something catastrophically wrong with my code or my understanding of async await?

Any help is much appreciated.

Edit: As per request in the comments I'm adding a simplified version of the method that calls GetServersStatusAsync and how I call that method (in a fire and forget fashion, but Login is wrapped with a try catch so that shouldn't be a big issue).

public async Task Login(Action<LoginResult> callback = null)
{

    LoginResult result = new LoginResult() { Success = false };
    try
    {
            var serverStatus = await GetServersStatusAsync();
            if (serverStatus.Any())
            {
                result.Success = true;
                callback?.Invoke(result);
                return;
            }
    }
    catch (Exception ex)
    {
        result.Message = Strings.UnknownException;
        _log.Error("Login failed due to unexpected exception", ex);
    }
    callback?.Invoke(result);
}
 _restClient.Login(OnLoginResponse);
1

There are 1 answers

0
Ryan On

but with ConfigureAwait(false) it's more of a 50/50.

I believe internal machinery of exceptions handling for default awaiter and ConfigureAwait(false)-based works the same way. So, it would be nice if you provided minimal reproducible example of your problem. I tried to follow your pattern:

  1. Convert a couple of exceptions to another one.

  2. Throw implicitly 3rd one as is.

  3. Catch all of them higher on the stack.

    foreach(int v in Enumerable.Range(1,10))
    try
    {
        await ExceptionDemo(v).ConfigureAwait(false);
    }
    catch (Exception e)
    {
        Console.WriteLine($"{e.GetType().Name}: {e.Message}");
    }
    
    
    
    static async Task ExceptionDemo(int value)
    {
    await Task.Delay(1000);
    try
    {
        throw Random.Shared.Next(0, 3) switch
        {
            0 => new ArgumentNullException($"{nameof(ArgumentNullException)}-{value}", (Exception?)null),
            1 => new InvalidOperationException($"{nameof(InvalidOperationException)}-{value}"),
            2 => new StackOverflowException($"{nameof(StackOverflowException)}-{value}")
        };
    }
    catch (Exception ex) when (ex is ArgumentNullException || ex is InvalidOperationException)
    {
        throw new ApplicationException(ex.Message);
    }
    }
    

And caught all of the exceptions. Async/await doesn't care about a way exceptions are thrown, nor in current thread, neither in continuation thread in case of async execution.

UPDATE

If you add an empty catch block, it would swallow some exceptions, so, in my case there are some missed exceptions:

ApplicationException: InvalidOperationException-1
ApplicationException: InvalidOperationException-2
ApplicationException: InvalidOperationException-3
ApplicationException: InvalidOperationException-4
ApplicationException: InvalidOperationException-6
ApplicationException: InvalidOperationException-9
ApplicationException: ArgumentNullException-10

But in your case they all should be thrown implicitly, and all are caught.