42

Related to this answer,

If I truly do want to "Fire and Forget" a method that does return a task, and (for simplicity) let's assume that the method isn't expected to throw any exceptions. I can use the extension method listed in the answer:

public static void Forget(this Task task)
{
}

Using this approach, if there are bugs in action of the Task that cause an exception to be thrown then when the unexpected exception is thrown, the exception will be swallowed and go unnoticed.

Question: Wouldn't it be more appropriate in this scenario for the extension method to be of the form:

public static async void Forget(this Task task)
{
    await task;
}

So that programming errors throw an exception and get escalated (usually bringing down the process).

In the case of a method with expected (and ignorable) exceptions, the method would need to become more elaborate (as an aside, any suggestions on how to construct a version of this method that would take a list of acceptable and ignorable exception types?)

Community
  • 1
  • 1
Matt Smith
  • 17,026
  • 7
  • 53
  • 103
  • For ideas about how to ignore exceptions of tasks, take a look a this: [A simpler way of ignoring specific types of exceptions when awaiting a Task](https://stackoverflow.com/questions/58465999/a-simpler-way-of-ignoring-specific-types-of-exceptions-when-awaiting-a-task) – Theodor Zoulias May 05 '20 at 05:54

3 Answers3

53

It depends on the semantics you want. If you want to ensure exceptions are noticed, then yes, you could await the task. But in that case it's not truly "fire and forget".

A true "fire and forget" - in the sense that you don't care about when it completes or whether it completes successfully or with error - is extremely rare.

Edit:

For handling exceptions:

public static async void Forget(this Task task, params Type[] acceptableExceptions)
{
  try
  {
    await task.ConfigureAwait(false);
  }
  catch (Exception ex)
  {
    // TODO: consider whether derived types are also acceptable.
    if (!acceptableExceptions.Contains(ex.GetType()))
      throw;
  }
}

Note that I recommend using await instead of ContinueWith. ContinueWith has a surprising default scheduler (as noted on my blog) and Task.Exception will wrap the actual exception in an AggregateException, making the error handling code more cumbersome.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 4
    So, how do I get the semantics of not caring when something finishes, but I do care about exceptions? Is it possible to have those semantics without `async void` at some level? – Matt Smith Apr 04 '14 at 13:57
  • 2
    Are you overthinking this or am I not getting it? It sounds like a `try`/`catch` is what you would need. Edited to show example code. – Stephen Cleary Apr 04 '14 at 14:50
  • I think @MattSmith's idea is to have `Forget` as a universal stub for fire-and-forget, without any `try/catch` inside. And leave the selective exception handling up to the task passed to `Forget`. This way, what bubbled uncaught would be immediate thrown on the current synchronization context. I like that :) – noseratio Apr 04 '14 at 15:01
  • 1
    @Noseratio and Stephen, Stephen your edit looks like exactly what I want. Thanks. And yes, as Noseratio, I'm planning on using this as a universal stub for fire-and-forget (except don't "forget"/swallow exceptions. – Matt Smith Apr 04 '14 at 15:35
  • @StephenCleary, with the `.ConfigureAwait(false)` in place, will any unexpected exceptions still be posted to the `SynchronizationContext` that was active at the beginning the method? – Matt Smith Apr 04 '14 at 16:30
  • 5
    The `catch` block will not run in the captured context (so any logging or whatever will not), but if the exception is not acceptable and is rethrown (`throw;`), then yes, the exception will be (re-)raised on the `SynchronizationContext` that was active at the beginning of `Forget`. – Stephen Cleary Apr 04 '14 at 16:36
  • @StephenCleary, to have the complete picture, is it re-raised through `SynchronizationContext.Send` of the captured context for `async void`? Tks. – noseratio Apr 04 '14 at 23:09
  • 3
    [It calls `Post`](http://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/AsyncMethodBuilder.cs#979), actually. – Stephen Cleary Apr 05 '14 at 00:30
  • @StephenCleary Aside from the reference source, have you been able to find this rather important behavior of async void methods documented anywhere in the official documentation? I have tried searching but only found a third party blog post: http://www.jaylee.org/post/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void.aspx – Søren Boisen Jun 03 '15 at 09:38
  • @SørenBoisen: I am not aware of any official documentation. – Stephen Cleary Jun 03 '15 at 12:11
4

In the linked question I initially wanted to use static void Forget(this Task task) in the following context:

var task = DoWorkAsync();
QueueAsync(task).Forget();

// ...

async Task QueueAsync(Task task)
{
    // keep failed/cancelled tasks in the list
    // they will be observed outside
    _pendingTasks.Add(task);
    await task;
    _pendingTasks.Remove(tasks)
}

It looked great, but then I realized that fatal exceptions possibly thrown by _pendingTasks.Add / _pendingTasks.Remove would be gone unobserved and lost, which is not good.

So I simply made QueueTask an async void method, what it essentially is:

var task = DoWorkAsync();
QueueAsync(task);

// ...

async void QueueAsync(Task task)
{
    // keep failed/cancelled tasks in the list
    // they will be observed outside
    _pendingTasks.Add(task);
    try
    {
        await task;
    }
    catch
    {
        return;
    }
    _pendingTasks.Remove(tasks)
}

As much as I don't like empty catch {}, I think it makes sense here.

In this scenario, keeping async Task QueueAsync() and using async void Forget(this Task task) like you propose would be on overkill, IMO.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
3

Yes if you were interested in whether or not the task threw an exception then you would need to await the result, but on the flipside, that pretty much defeats the purpose of "fire & forget".

In the scenario where you want to know if something bad happened then the recommended way of doing this is to use a continuation e.g.

public static void ForgetOrThrow(this Task task)
{
    task.ContinueWith((t) => {
        Console.WriteLine(t.Exception);
    }, TaskContinuationOptions.OnlyOnFaulted);
}
James
  • 80,725
  • 18
  • 167
  • 237
  • "if you make Forget an async method then you basically reintroduce the same issue"? What do you mean--it does suppress the warning and have the same behavior in the case of no exceptions thrown. I guess I want "Fire and Forget", except I don't want to ignore bugs. – Matt Smith Apr 04 '14 at 13:53
  • @MattSmith it suppresses the warning? The warning is due to the fact that externally there is no `await` call on `WorkAsync()` from inside an `async` method. The `Forget` extension method (which does nothing) suppresses the warning because it's a non-async method. Making it `async` again *should* be re-introducing the warning as you are back to calling an `async` method and not calling `await` (unless extension methods are treated differently...). – James Apr 04 '14 at 13:58
  • 3
    it is `async void` so there is nothing that can be awaited. – Matt Smith Apr 04 '14 at 13:59
  • 1
    Albahari proposed this same idea in Chapter 23 of C# 5.0 In a Nutshell as a way to 'swallow' a task's unhandled exception, but noting that you could do something with the exception, such as logging it...so I would imagine it could be extensible to your idea of ignoring some and re-throwing others. Instead of just `throw t.Exception` you could 'observe' it first (`var ignore = t.Exception;` and if not an acceptable one, _then_ throw it: `if(reallyBad) {throw t.Exception;}` This allows the acceptable ones to be simply swallowed. – mdisibio Apr 04 '14 at 14:48
  • @mdisibio Arh but therein lies the complication. You don't want to simply rethrow the exception in the continuation task, then you will have replaced one unhandled exception with another. You need to somehow post it back to the synchronization context, like Stephen Clearys suggestion apparently achieves. – Søren Boisen Jun 03 '15 at 08:45
  • @SørenBoisen Stephen posted his outstanding answer after _James'_, so from a year-old dialogue standpoint, his answer supercedes James' answer and my comment. That said, (and I am nowhere near the expert Cleary or anyone else on this page is) I believe by observing the antecedent exception, it is no longer considered 'unhandled'. To quote Albahari: _"A safe pattern is to re-throw antecendent exceptions. As long as the continuation is Waited upon, the exception will be propagate and rethrown to the Waiter."_ (p.942) – mdisibio Jun 03 '15 at 13:44
  • @mdisibio I wanted to point out for posterity that simply rethrowing won't cut it. I probably should not have addressed it specifically to you. As your quote states "As long as the continuation is Waited upon" - the context here is fire-and-forget, so you wouldn't normally be waiting on the continuation. As indeed is the case for the code in this answer :) – Søren Boisen Jun 03 '15 at 14:46