Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
289 views
in Technique[技术] by (71.8m points)

c# - Good pattern for exception handling when using async calls

I want to consume an Web API and I see many people recommending System.Net.Http.HttpClient.

That's fine... but I have only VS-2010, so I cannot use async/await just yet. Instead, I guess I could use Task<TResult> in combination to ContinueWith. So I tried this piece of code:

var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(
    new MediaTypeWithQualityHeaderValue("application/json"));

client.GetStringAsync(STR_URL_SERVER_API_USERS).ContinueWith(task =>
{                 
   var usersResultString = task.Result;
   lbUsers.DataSource = JsonConvert.DeserializeObject<List<string>>(usersResultString);
});

My first observation was to realize that it doesn't generate any error if URL is not available, but maybe there will be more errors like this...

So I am trying to find a way to handle exceptions for such async calls (particularly for HttpClient). I noticed that "Task" has IsFaulted property and an AggregateException which maybe could be used, but I am not sure yet how.

Another observation was that GetStringAsync returns Task<string>, but GetAsync returns Task<HttpResponseMessage>. The latter could be maybe more useful, since it presents a StatusCode.

Could you share a pattern on how to use the async calls correctly and handle exceptions in a good way? Basic explanation would be appreciated as well.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I would not use a separate ContinueWith continuation for successful and faulted scenarios. I'd rather handle both cases in a single place, using try/catch:

task.ContinueWith(t =>
   {
      try
      {
          // this would re-throw an exception from task, if any
          var result = t.Result;
          // process result
          lbUsers.DataSource = JsonConvert.DeserializeObject<List<string>>(result);
      }
      catch (Exception ex)
      {
          MessageBox.Show(ex.Message);
          lbUsers.Clear();
          lbUsers.Items.Add("Error loading users!");
      }
   }, 
   CancellationToken.None, 
   TaskContinuationOptions.None, 
   TaskScheduler.FromCurrentSynchronizationContext()
);

If t is a non-generic Task (rather than a Task<TResult>), you can do t.GetAwaiter().GetResult() to re-throw the original exception inside the ContinueWith lambda; t.Wait() would work too. Be prepared to handle AggregatedException, you can get to the inner exception with something like this:

catch (Exception ex)
{
    while (ex is AggregatedException && ex.InnerException != null)
        ex = ex.InnerException;

    MessageBox.Show(ex.Message);
}

If you're dealing with a series of ContinueWith, usually you don't have to handle exceptions inside each ContinueWith. Do it once for the outermost resulting task, e.g.:

void GetThreePagesV1()
{
    var httpClient = new HttpClient();
    var finalTask = httpClient.GetStringAsync("http://example.com")
        .ContinueWith((task1) =>
            {
                var page1 = task1.Result;
                return httpClient.GetStringAsync("http://example.net")
                    .ContinueWith((task2) =>
                        {
                            var page2 = task2.Result;
                            return httpClient.GetStringAsync("http://example.org")
                                .ContinueWith((task3) =>
                                    {
                                        var page3 = task3.Result;
                                        return page1 + page2 + page3;
                                    }, TaskContinuationOptions.ExecuteSynchronously);
                        }, TaskContinuationOptions.ExecuteSynchronously).Unwrap();
            }, TaskContinuationOptions.ExecuteSynchronously).Unwrap()
        .ContinueWith((resultTask) =>
            {
                httpClient.Dispose();
                string result = resultTask.Result;
                try
                {
                    MessageBox.Show(result);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
            },
            CancellationToken.None,
            TaskContinuationOptions.None,
            TaskScheduler.FromCurrentSynchronizationContext());
}

Any exceptions thrown inside inner tasks will propagate to the outermost ContinueWith lambda as you're accessing the results of the inner tasks (taskN.Result).

This code is functional, but it's also ugly and non-readable. JavaScript developers call it The Callback Pyramid of Doom. They have Promises to deal with it. C# developers have async/await, which you're unfortunately not able to use because of the VS2010 restrain.

IMO, the closest thing to the JavaScript Promises in TPL is Stephen Toub's Then pattern. And the closest thing to async/await in C# 4.0 is his Iterate pattern from the same blog post, which uses the C# yield feature.

Using the Iterate pattern, the above code could be rewritten in a more readable way. Note that inside GetThreePagesHelper you can use all the familiar synchronous code statements like using, for, while, try/catch etc. It is however important to understand the asynchronous code flow of this pattern:

void GetThreePagesV2()
{
    Iterate(GetThreePagesHelper()).ContinueWith((iteratorTask) =>
        {
            try
            {
                var lastTask = (Task<string>)iteratorTask.Result;

                var result = lastTask.Result;
                MessageBox.Show(result);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                throw;
            }
        },
        CancellationToken.None,
        TaskContinuationOptions.None,
        TaskScheduler.FromCurrentSynchronizationContext());
}

IEnumerable<Task> GetThreePagesHelper()
{
    // now you can use "foreach", "using" etc
    using (var httpClient = new HttpClient())
    {
        var task1 = httpClient.GetStringAsync("http://example.com");
        yield return task1;
        var page1 = task1.Result;

        var task2 = httpClient.GetStringAsync("http://example.net");
        yield return task2;
        var page2 = task2.Result;

        var task3 = httpClient.GetStringAsync("http://example.org");
        yield return task3;
        var page3 = task3.Result;

        yield return Task.Delay(1000);

        var resultTcs = new TaskCompletionSource<string>();
        resultTcs.SetResult(page1 + page1 + page3);
        yield return resultTcs.Task;
    }
}

/// <summary>
/// A slightly modified version of Iterate from 
/// http://blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx
/// </summary>
public static Task<Task> Iterate(IEnumerable<Task> asyncIterator)
{
    if (asyncIterator == null)
        throw new ArgumentNullException("asyncIterator");

    var enumerator = asyncIterator.GetEnumerator();
    if (enumerator == null)
        throw new InvalidOperationException("asyncIterator.GetEnumerator");

    var tcs = new TaskCompletionSource<Task>();

    Action<Task> nextStep = null;
    nextStep = (previousTask) =>
    {
        if (previousTask != null && previousTask.Exception != null)
            tcs.SetException(previousTask.Exception);

        if (enumerator.MoveNext())
        {
            enumerator.Current.ContinueWith(nextStep,
                TaskContinuationOptions.ExecuteSynchronously);
        }
        else
        {
            tcs.SetResult(previousTask);
        }
    };

    nextStep(null);
    return tcs.Task;
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...