Eric J. Smith
Eric J. Smith

Reputation: 971

Exception handling with async delegate

I am trying to figure out what I am doing wrong here. I'm sure it's something stupid, but I could use some extra eyes to figure out what it is. I'm trying to create a worker service that you can assign a work action delegate to. When I try to catch the exception thrown by the work action delegate the handler isn't invoked. I think it is because my action is actually an async method that returns Task and it isn't being awaited. But how am I supposed to know that the delegate is async? Why does C# let me assign a method that returns a Task to a action variable that is supposed to return void?

Any help would be appreciated!

[Fact]
public async Task CanCatchExceptions() {
    var worker = new Worker {
        WorkAction = async item => await Throws()
    };
    worker.DoWork(new WorkItem { Id = 1 });
}

public class Worker {
    // would prefer to keep work action simple and not require a Task.
    public Action<WorkItem> WorkAction { get; set; }

    public void DoWork(WorkItem item) {
        try {
            WorkAction(item);
        } catch {
            Debug.WriteLine("Handled");
        }
    }
}

public class WorkItem {
    public int Id { get; set; }
}

public async Task Throws() {
    throw new ApplicationException();
}

Upvotes: 4

Views: 2635

Answers (2)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149538

Why does C# let me assign a method that returns a Task to a action variable that is supposed to return void?

Because async void is a legitimate use case for async event handlers, that's why the compiler permits the void returning Action<T> and doesn't complain. Once you're aware of that fact, you can explicitly use a Func<Task> which will create the desired Task returning method overload.

As you say to you prefer to keep work action "simple" and not require a task, consider supplying an async overload which will behave properly with the async control flow:

public Func<WorkItem, Task> WorkAsync { get; set; }

public async Task WorkAsync(WorkItem item)
{
      try
      {
            await WorkAsync(item)
      }
      catch (Exception e)
      {
           // Handle
      }
}

Note this may be in a completely different class (seperate the async worker with the sync one), as per your choice.

Upvotes: 2

Stephen Cleary
Stephen Cleary

Reputation: 456587

Lambdas without return values may be cast to a task-returning method (e.g., Func<Task>) or a void-returning method (e.g., Action). Note that when casting to a void-returning method, the actual method for that lambda is an async void method, with all the problems that come along with async void methods. I describe some of the problems of async void in my best practices article; one of them is that you can't catch exceptions with try.

The best option is to change your work items to return Task, which is a much more natural representation of asynchronous methods.

Upvotes: 5

Related Questions