John
John

Reputation: 33

How to properly catch exceptions in multiple async method calls?

I have some async method:

public async Task<Object> MethodAsync()
{
    return await MakeHttpRequestAsync();
}

I need to make multiple calls of this method. One of them can throw an exception. But I need to keep all possible returned Objects from other method calls that succeeded.

Option 1:

Task t1 = MethodAsync();
Task t2 = MethodAsync();
try
{
    Task.WaitAll(new Task[]{t1,t2});
}
catch(Exception e)
{
   ...
}
//are t1 and t2 finished execution here with success or exception?

Option 2:

Task t1 = MethodAsync();
Task t2 = MethodAsync();
try
{
    t1.Wait()
}
catch(Exception e)
{
   ...
}

try
{
    t2.Wait()
}
catch(Exception e)
{
   ...
}
//what happens if t2 throws exception before t1 finished?

Any other options?

edit: Thought about third option:

    public async Task<Tuple<Object, Object>> MultipleAsyncCallsAsync(some arguments)
    {
            Object result1 = null, result2 = null;
            try
            {
                result1 = await MethodAsync();
            }
            catch(Exception e)
            {

            }
            try
            {
                result2 = await MethodAsync();
            }
            catch (Exception e)
            {

            }
            return Tuple.Create(result1, result2);
    }

edit #2:

Looks like third option is not working, but it's working this way:

public async Task<Tuple<object, object>> MultipleAsyncCallsAsync(some arguments)
    {
            object result1 = null, result2 = null;
            Task<object> t1 = null, t2 = null;
            t1 = Task.Run(()=> MethodAsync());
            t2 = Task.Run(()=> MethodAsync());
            try
            {
                result1 = await t1;
            }
            catch(Exception e)
            {

            }
            try
            {
                result2 = await t2;
            }
            catch (Exception e)
            {

            }
            return Tuple.Create(result1, result2);
    }

Upvotes: 2

Views: 1215

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456437

Whenever there's a question about this kind of procedure ("call n methods, (a)wait for them all to complete, and then retrieve the results for each method and do something with them"), a better solution is almost always to create a higher-order method.

For example, if you want to do some kind of logging or something but otherwise ignore exceptions, then you can create a higher-order method like this:

public async Task<T> LogAndIgnoreExceptions(Task<T> task) where T : class
{
  try
  {
    return await task;
  }
  catch (Exception ex)
  {
    Log(ex);
    return null;
  }
}

Then you can use Task.WhenAll in a very natural way:

var results = await Task.WhenAll(
    LogAndIgnoreExceptions(MethodAsync()),
    LogAndIgnoreExceptions(MethodAsync())
);
if (results[0] is not null)
{
  T result = results[0];
}
else
{
  // First call had an error.
}

Or, if you want to preserve the exception details and results, what you want is a little helper type like Try to enable railway programming:

var results = await Task.WhenAll(
    Try.Create(MethodAsync()),
    Try.Create(MethodAsync())
);
if (results[0].IsValue)
{
  T result = results[0].Value;
}
else
{
  Exception exception = results[0].Exception;
}

Upvotes: 2

Peter Bons
Peter Bons

Reputation: 29720

Put the tasks in an array and inspect their status like this:

    var t1 = MethodAsync();
    var t2 = MethodAsync();
    
    var tasks = new[] {t1, t2};
    
    try
    {
        await Task.WhenAll(tasks);
    }
    catch
    {
        var failedTasks = tasks.Where(t => t.IsFaulted);
        var allExceptions = failedTasks.Select(t => t.Exception?.Flatten());
    }
    
    var results = tasks.Where(t => t.IsCompletedSuccessfully).Select(t => t.Result);

What is does is it awaits all tasks and then uses the task status (using IsCompletedSuccessfully ect.) to get the results of all the tasks that did complete succesfully.

You can determine the failed tasks using IsFaulted and get their exception using the tasks Exception property.

If you want to catch/inspect the exception before the call to Task.WhenAll(..) is completed wrap the code inside MethodAsync in a try/catch:

public async Task<Object> MethodAsync()
{
    try
    {
        ..
    }
    catch(Exception ex)
    {
        //handle or throw
        return null;
    }
}

Or, if you cannot modify MethodAsync for some reason, you can wrap the method in a try/catch using this:

Func<Task<Object>, Task<Object>> TryExecute = new Func<Task<Object>, Task<object>>(async t =>
{
    try
    {
        return await t;
    }
    catch (Exception ex)
    {
        //handle exception
        return null;
    }
});

and use it like this:

    var t1 = TryExecute(MethodAsync());
    var t2 = TryExecute(MethodAsyncF());

    var tasks = new[] {t1, t2};

    await Task.WhenAll(tasks);

Upvotes: 3

Related Questions