ContinueWith TaskContinuationOptions.OnlyOnFaulted does not seem to catch an exception thrown from a started task

好久不见. 提交于 2019-12-05 13:16:10

First of all, you aren't using OnlyOnFaulted correctly. When you use ContinueWith on a task you don't really change that task, you get back a task continuation (which in your case you disregard).

If the original task faulted (i.e. had an exception thrown inside it) it would stay faulted (so calling Wait() on it would always rethrow the exception). The continuation however would run after the task faulted and handle the exception.

That means that in your code you do handle the exception, but you're also rethrowing it with Wait(). The correct code should look like this:

Task originalTask = Task.Run(() => throw new Exception());
Task continuationTask = originalTask.ContinueWith(t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
continuationTask.Wait()
// Both tasks completed. No exception rethrown

Now, as Yuval Itzchakov pointed out, you can handle the exception wherever you want but it would better if you were utilizing async-await to wait asynchronously if you can (you can't in Main) instead of blocking with Wait():

try
{
    await originalTask;
}
catch (Exception e)
{
    // handle exception
}

Am I using the TaskContinutationOptions.OnlyOnFaulted correctly or is it always better to handle the exception in the task method itself? I would like the main thread to continue even if task runs into exception.

You can handle exceptions either way, inside or outside. It's a matter of preference and usually depends on the use-case.

Note that one thing you aren't doing is keeping a reference to your continuation. You're using Task.Wait on the original task which propogates the exception regardless of the fact that you have a continuation which handles it.

One thing that bothers me is that you're using Task.Wait which synchronously waits instead of await which asynchronously waits. Thats the reason for the AggregationException. More so, you shouldn't block on asynchronous operations, as that will lead you down a rabit hole you probably don't want to go, with all sorts of synchronization context problems.

What i would personally do, is use await inside of ContinueWith, because its the less verbose option. Also, i'd use Task.Run over Task.Factory.StartNew:

var task = Task.Run(() => 
{
    Thread.Sleep(1000);
    throw new InvalidOperationException();
}

// Do more stuff here until you want to await the task.

try
{           
    await task;
}
catch (InvalidOperationException ioe)
{
    // Log.
}

The original question happens to run its taskAction on a separate thread. That might not always be the case.

The answer by i3arnon solves the problem well. What if we do not want to use a separate thread? If we want to simply start a task, running it synchronously until we hit IO or a delay, and then continue about our own business. Only at the very end, we will wait for the task to complete.

How do we await the task with rethrowing its exception? Instead of wrapping it in Task.Run(), we will use an empty continuation that will always succeed when the task finishes for any reason.

// Local function that waits a moment before throwing
async Task ThrowInAMoment()
{
    await Task.Delay(1000);
    Console.WriteLine("Task waited for a sec");
    throw new Exception("Throwing for example");
}

// Start the task without waiting for it to complete
var t = ThrowInAMoment();

// We reach this line as soon as ThrowInAMoment() can no longer proceed synchronously
// This is as soon as it hits its "await Delay(1000)"

// Handle exceptions in the task
t.ContinueWith(x => Console.WriteLine("In the on Faulted continue with code. Catched exception from the task."+  t.Exception), TaskContinuationOptions.OnlyOnFaulted);

// Continue about our own business
Console.WriteLine("Main thread waiting for 4 sec");
Thread.Sleep(4000);
Console.WriteLine("Wait of 4 secs complete..checking if task is completed?");
Console.WriteLine("Task State: " + t.Status);

// Now we want to wait for the original task to finish
// But we do not care about its exceptions, as they are already being handled
// We can use ContinueWith() to get a task that will be in the completed state regardless of HOW the original task finished (RanToCompletion, Faulted, Canceled)
await t.ContinueWith(task => {});

// Could use .Wait() instead of await if you want to wait synchronously for some reason

It looks like you are on the right track. I have ran your code and get the same results. My suggestion is to use the try/catch directly from inside the action. This will make your code clearer and allow you to log things like which thread it came from, which may be different in the continuation path.

var taskAction = new Action(() =>
{
    try{
        Thread.Sleep(1000); 
        Console.WriteLine("Task Waited for a sec");
        throw (new Exception("throwing for example"));
    }
    catch (Exception e)
    {
        Console.WriteLine("In the on Faulted continue with code. Catched exception from the task."+  e);
    }

});
Task t = Task.Factory.StartNew(taskAction);
Console.WriteLine("Main thread waiting for 4 sec");
Thread.Sleep(4000);
Console.WriteLine("Wait of 4 secs complete..checking if task is completed?");
Console.WriteLine("Task State: " + t.Status);
await t;

[Edit] Removed outer exception handler.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!