johnildergleidisson
johnildergleidisson

Reputation: 2117

How to handle exception with explicitly constructed Tasks

I have a project which executes several operations in a very similar manner (subscribes to completion event, executes task, unsubscribes from the completion event and also deals with cancellation, timeouts etc) so I decided to write a utility class that handles that execution. However I came across a scenario which I don't understand and as a result have no idea how to fix.

This oversimplified code illustrates the problem:

class Program
{
    static void Main(string[] args)
    {
        Do();
        Console.Read();
    }

    private static async Task Do()
    {
        var task = new Task(async() => await Operation()/*this throws and terminates the application*/);

        try
        {
            await OperationExecuter.ExecuteAsync(task);
        }
        catch (InvalidOperationException)
        {
            //I expected the exception to be caught here
        }
    }


    static async Task Operation()
    {
        await Task.Delay(1000);
        throw new InvalidOperationException();
    }
}

class OperationExecuter
{
    public static async Task ExecuteAsync(Task task)
    {
        task.Start();
        await task; //I expected the exception to be unwrapped and thrown here
    }
}

I also tried to have the task like var task = new Task(() => Operation()); but the exception is never handled (although it doesn't terminate the application as it isn't raised in the main thread).

How would I handle the exception properly?

Changing the implementation to take an action yields to same results:

class Program
{
    static void Main(string[] args)
    {
        Do();
        Console.Read();
    }

    private static async Task Do()
    {
        var action = new Action(async () => await Operation() /*this throws and terminates the application*/);

        try
        {
            await OperationExecuter.ExecuteAsync(action);
        }
        catch (InvalidOperationException)
        {
            //I expected the exception to be caught here
        }
    }


    static async Task Operation()
    {
        await Task.Delay(1000);
        throw new InvalidOperationException();
    }
}

class OperationExecuter
{
    public static async Task ExecuteAsync(Action action)
    {
        await Task.Run(action); //I expected the exception to be unwrapped and thrown here
    }
}

For the curious ones a more realistic OperationExecuter would be something along the lines of:

class Program
{
    static void Main(string[] args)
    {
        Do();
        Do2();
        Console.Read();
    }

    private static async Task Do()
    {
        var service = new Service(new Hardware());
        try
        {
            await
                OperationExecuter.ExecuteAsync(service, handler => service.Operation1Completed += handler,
                    handler => service.Operation1Completed += handler, async () => await service.Operation1(),
                    CancellationToken.None);
        }
        catch (InvalidOperationException)
        {
            //Exception is caught!!!
        }
    }

    private static async Task Do2()
    {
        var service = new Service(new Hardware());
        try
        {
            await
                OperationExecuter.ExecuteAsync(service, handler => service.Operation1Completed += handler,
                    handler => service.Operation1Completed += handler, async () => await service.Operation2(60),
                    CancellationToken.None);
        }
        catch (InvalidOperationException)
        {
            //Exception is caught!!!
        }
    }
}

internal class OperationExecuter
{
    public static async Task ExecuteAsync(Service service, Action<EventHandler> subscriptionAction,
        Action<EventHandler> unsubscriptionAction, Func<Task> sendCommandAction, CancellationToken cancellationToken)
    {
        var commandCompletionSource = new TaskCompletionSource<bool>();
        var hardwareFailureCompletionSource = new TaskCompletionSource<bool>();

        cancellationToken.Register(() => commandCompletionSource.SetCanceled());

        var eventHandler = new EventHandler((sender, args) =>
        {
            commandCompletionSource.SetResult(true);
        });

        service.HardwareFailure += (sender, args) => hardwareFailureCompletionSource.SetResult(false);

        subscriptionAction(eventHandler);

        try
        {
            await Task.Run(sendCommandAction, cancellationToken);
            await Task.WhenAny(commandCompletionSource.Task, hardwareFailureCompletionSource.Task);

            //same for disconnection, etc
            if (hardwareFailureCompletionSource.Task.IsCompleted)
            {
                throw new HardwareFailureException();
            }
        }
        finally
        {
            unsubscriptionAction(eventHandler);
        }
    }
}

class HardwareFailureException : Exception
{
}

class Service
{
    private readonly Hardware hardware;

    public Service(Hardware hardware)
    {
        this.hardware = hardware;
    }

    public async Task Operation1() //something like sending command to hardware
    {
        await Task.Delay(1000);
        throw new InvalidOperationException();
    }

    public event EventHandler Operation1Completed;

    public async Task Operation2(int someParameter)
    {
        await Task.Delay(1000);
        throw new InvalidOperationException();
    }

    public event EventHandler Operation2Completed;

    public event EventHandler LostConnection;

    public event EventHandler HardwareFailure;
}

class Hardware
{
}

Upvotes: 3

Views: 354

Answers (1)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149618

The problem is due to the fact that you actually create a Task<Task>, and you only await the outter Task. This is one of the reasons as to why you shouldn't be using the Task constructor. Instead, use Task.Run, which is aware of that and will unwrap the outter task for you:

private static async Task Do()
{
    var task = Task.Run(async() => await Operation());
    try
    {
        await OperationExecuter.ExecuteAsync(task);
    }
    catch (InvalidOperationException)
    {
        //I expected the exception to be caught here
    }
}

Edit:

@Servy points out correctly that, unless there is a particular good reason you're wrapping your Task with Task.Run, you can save that all together and simply await on the created Task and save yourself the unwrapping trouble all together:

public class OperationExecuter
{
    public static async Task ExecuteAsync(Func<Task> func)
    {
        await func();
    }
}

Upvotes: 4

Related Questions