Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ContinueWith and TaskCancellation - How to return default-values if task fails?

I read a few threads about TaskCancellations.. However, I cannot find a solution for a simple question: How do I get a default value when my task fails?

I cannot (!) modify the task itself and put a try catch wrapper around it. I could of course put a try-catch around await, but I would like to handle this with ContinueWith - if possible.

public Task<List<string>> LoadExample()
{
    Task<List<string>> task = LoadMyExampleTask();
    task.ContinueWith(t => default(List<string>), TaskContinuationOptions.OnlyOnFaulted);
    return task;
}

I thought this would be the correct way to deal with the problem. However, my application throws a JsonParseException (which is called in LoadMyExampleTask). I would expect to get null or (even better) an empty list.

In fact, all I want is:

var emptyOrFilledList = await LoadExample(); // guaranteed no exception thrown

Based on Luaan's great answer I wrote an extension method with a defaultValue-option:

public static Task<T> DefaultIfFaulted<T>(this Task<T> @this, T defaultValue = default(T))
{
   return @this.ContinueWith(t => t.IsCompleted ? t.Result : defaultValue);
}

Edit: await myTask.DefaultifFaulted() just throwed a

[ERROR] FATAL UNHANDLED EXCEPTION: System.AggregateException

Are you sure that every exception is caught?

like image 508
Frame91 Avatar asked Dec 10 '15 09:12

Frame91


People also ask

What does calling Task ContinueWith () do?

ContinueWith(Action<Task,Object>, Object, TaskScheduler)Creates a continuation that receives caller-supplied state information and executes asynchronously when the target Task completes.

What is Task continuation?

A continuation task (also known just as a continuation) is an asynchronous task that's invoked by another task, known as the antecedent, when the antecedent finishes.

How do I run a Task in C#?

To start a task in C#, follow any of the below given ways. Use a delegate to start a task. Task t = new Task(delegate { PrintMessage(); }); t. Start();


2 Answers

If you want that, you must not return the original task - you need to return the continuation.

public Task<List<string>> LoadExample()
{
    Task<List<string>> task = LoadMyExampleTask();
    return task.ContinueWith(t => 
            t.IsFaulted || t.IsCanceled ? default(List<string>) : t.Result);
}

Your original code did allow the continuation to run when the original task faulted, but you didn't read the status of that task - the fact that a task has a continuation which handles errors is entirely irrelevant to what an await on the original task will do.

Of course, it's rather easy to make this into a generic helper method:

public static Task<T> DefaultIfFaulted<T>(this Task<T> @this)
{
  return @this.ContinueWith (t => t.IsCanceled || t.IsFaulted ? default(T) : t.Result);
}
like image 125
Luaan Avatar answered Oct 14 '22 17:10

Luaan


As promised, here are the DefaultIfFaulted<T> variants which are true to their name (and the title of this question). They preserve the antecedent task's behavior unless it's faulted (specifically, cancellation is propagated rather than ignored or masked by an AggregateException):

Old-school (.NET 4.0) way:

public static Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
    // The continuation simply returns the antecedent task unless it's faulted.
    Task<Task<T>> continuation = task.ContinueWith(
        t => (t.Status == TaskStatus.Faulted) ? Task.FromResult(default(T)) : t,
        TaskContinuationOptions.ExecuteSynchronously
    );

    return continuation.Unwrap();
}

Async/await way (simple but slower):

public static async Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
    try
    {
        return await task.ConfigureAwait(false);
    }
    catch (Exception ex) when (!(ex is OperationCanceledException))
    {
        return default(T);
    }
}

Async/await way (perf almost identical to Unwrap):

public static async Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
    // Await completion regardless of resulting Status (alternatively you can use try/catch).
    await task
        .ContinueWith(_ => { }, TaskContinuationOptions.ExecuteSynchronously)
        .ConfigureAwait(false);

    return task.Status != TaskStatus.Faulted
        // This await preserves the task's behaviour
        // in all cases other than faulted.
        ? await task.ConfigureAwait(continueOnCapturedContext: false)
        : default(T);
}

Tests (passed by all of the above):

using Xunit;

[Fact]
public async Task DefaultIfFaultedTest()
{
    var success = Task.Run(() => 42);
    var faulted = Task.Run(new Func<int>(() => { throw new InvalidOperationException(); }));

    Assert.Equal(42, await success.DefaultIfFaulted());
    Assert.Equal(0, await faulted.DefaultIfFaulted());

    await Assert.ThrowsAsync<TaskCanceledException>(() =>
    {
        var tcs = new TaskCompletionSource<int>();

        tcs.SetCanceled();

        return tcs.Task.DefaultIfFaulted();
    });
}
like image 34
Kirill Shlenskiy Avatar answered Oct 14 '22 18:10

Kirill Shlenskiy