8

I'm working on a Web Hook in .NET 4.0 that will run a lambda asynchronously and then post the result to a given URI when it is finished.

I've got that to work, but now I want the Task to handle any exceptions that are thrown, and am finding it difficult to stop them from reaching the parent.

Here's part of my code:

private readonly Func<T> _startTask;
private readonly string _responseUri;

public Task<T> Begin()
{
    var task = new Task<T>(_startTask);
    task.ContinueWith<T>(End);
    task.Start();
    return task;
}

private T End(Task<T> task)
{
    if (task.IsFaulted)
    {
        return HandleException(task);
    }

    var result = task.Result;
    WebHookResponse.Respond(result, _responseUri);
    return result;
}

private T HandleException(Task<T> task)
{
    WebHookResponse.HandleException(task.Exception.InnerException, _responseUri);
    return null;
}

An alternative version that I have tried calls ContinueWith() twice to register one continuation to run OnlyOnRanToCompletion and one to run OnlyOnFaulted. (I'm not sure if calling ContinueWith() twice is correct.):

public Task<T> Begin()
{
    var task = new Task<T>(_startTask);
    task.ContinueWith<T>(End, TaskContinuationOptions.OnlyOnRanToCompletion);
    task.ContinueWith<T>(HandleException, TaskContinuationOptions.OnlyOnFaulted);
    task.Start();
    return task;
}

private T End(Task<T> task)
{
    var result = task.Result;
    WebHookResponse.Respond(result, _responseUri);
    return result;
}

private T HandleException(Task<T> task)
{
    WebHookResponse.HandleException(task.Exception.InnerException, _responseUri);
    return null;
}

So basically I want a way for each Task handle its own exceptions via a continuation function. As it stands the HandlException continuation function is never being called in either of the above examples.

I am causing the exceptions in a test case, and I should mention that I am using a Tasks.WaitAll(tasks); call on an array of Tasks to make sure all of the tasks are complete before making my assertions, and I am not sure if that call makes a difference to how exceptions are handled by the Tasks. Currently WaitAll throws an AggregationException which aggregates the exceptions for each of the Tasks because they aren't being handled by the HandleException continuation function.

Martin Owen
  • 5,221
  • 3
  • 35
  • 31
  • I don't see the problem. What behavior are you seeing? – Stephen Cleary Jul 19 '10 at 10:37
  • Sorry, the HandleException continuation function isn't being called. An AggregationException is thrown by the Tasks.WaitAll(tasks); call. I don't want to see an AggregationException. I just updated the question. – Martin Owen Jul 19 '10 at 10:40
  • @Martin Owen Hi, my HandleExceptionContinuation isnt being called too and my UnobservedTaskException event isnt being fired too here : http://stackoverflow.com/questions/11831844/unobservedtaskexception-being-throw-but-it-is-handled-by-a-taskscheduler-unobser do you found some solution ? – newway Aug 07 '12 at 19:14
  • Also beware of a potential memory leak if you use continuations which are not run - in combination with a cancellation token. – urbanhusky May 04 '15 at 13:16

3 Answers3

3

A task continuation that observes the task's exception doesn't handle the exception. It still happens on wherever you wait on the task to finish.

You said you were calling WaitAll(tasks) before asserting. I'm betting that your continuation would have run if you gave it enough time, but the exception on WaitAll() is usually going to occur before your continuation runs. So your asserts probably failed before your continuation was given a chance to finish its work.

Ross
  • 1,553
  • 1
  • 14
  • 22
1

I use this approach because it provides for a nice declarative fluent coding style and does not litter your code with the exception handling aspects.

class Program
{
    static void Main()
    {
        Task.Factory.StartNew(
            () =>
                {
                    throw new Exception();
                })
            .Success(() => Console.WriteLine("Works"))
            .Fail((ex) => Console.WriteLine("Fails")).Wait();

        Console.ReadKey();
    }
}

public static class TaskExtensions
{
    public static Task Success(this Task task, Action onSuccess)
    {
        task.ContinueWith((t) =>
        {
            if (!t.IsFaulted)
            {
                onSuccess();
            }
        });

        return task;
    }

    public static Task Fail(this Task task, Action<Exception> onFail)
    {
        return task.ContinueWith(
            (t) =>
            {
                if (t.IsFaulted)
                {
                    t.Exception.Handle(x => true);
                    onFail(t.Exception.Flatten());
                }
            });
    }
}
Jason Bowers
  • 496
  • 6
  • 12
1

Perhaps, to the answer of Henk Holterman, the order makes a difference. That is,

var task = new Task<T>(_startTask);
task = task.ContinueWith<T>(HandleException, TaskContinuationOptions.OnlyOnFaulted);
task = task.ContinueWith<T>(End, TaskContinuationOptions.OnlyOnRanToCompletion);
task.Start();

would assure that HandleException would run when necessary.

Albino Cordeiro
  • 1,554
  • 1
  • 17
  • 25
  • No, it doesn't. Both continuations will be called if necessary, regardless of the order in which you called `ContinueWith` (obviously only one each time since their `TaskContinuationOptions` are mutually exclusive). – Ohad Schneider Feb 13 '12 at 16:34
  • If you did it in that order, exceptions that aggregated in End would not be handled. HandleException would always have to be the last one called, like a sweeper in soccer is behind all the defense so it can sweep up anything that may get through, same thing. – Despertar May 25 '12 at 22:45