4

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.

Learner
  • 3,297
  • 4
  • 37
  • 62

1 Answers1

3

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;
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Thanks again for this great and complete answer. As far as I can see, there are so many details in your code which I don't understand... and poor me, I am just a beginner. I must admit I find it pretty difficult to grasp all this TPL stuff... especially in VS2010 world. Very strange I didn't give it up earlier and go with something simpler for my world like HttpWebRequest :) ... oh boy, tough decision to make – Learner Mar 10 '14 at 12:35
  • @Cristi, using `HttpWebRequest` would not make a huge difference. I.e., you could just do `var text = httpClient.GetStringAsync("http://example.net").Result`, which would be a blocking synchronous call (because of `Result`). Feel free to clarify whatever is not clear here or in a separate question. Also, try stepping this code through in the debugger, it may help to understand it. – noseratio Mar 10 '14 at 12:42
  • You are definitely true! One small question: why my code from the question does not throw any exception in case of URL is not available? – Learner Mar 10 '14 at 14:55
  • Btw, I guess in the worst case scenario I could opt for javaScript/jQuery, instead of WinForms app, but let's see... I would prefer desktop app for now. – Learner Mar 10 '14 at 15:00
  • 1
    Ok, I found an answer to my previous question: http://stackoverflow.com/questions/17151215/silent-exceptions-in-task-factory-startnew-when-using-a-long-running-background – Learner Mar 10 '14 at 20:43
  • 1
    @Cristi, exception handling in .NET 4.0 works slightly differently from v4.5, check [Tasks and Unhandled Exceptions](http://blogs.msdn.com/b/pfxteam/archive/2009/05/31/9674669.aspx). – noseratio Mar 10 '14 at 21:18