0

Here is my test:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace asynktest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var t = Test1(true);
            await t; //throws InvalidOperationException here. correct
            t = Test1(false);
            await t; //throws NotImplementedException here. correct

            t = Test2(true); //throws InvalidOperationException here. wrong
            await t;
            t = Test2(false); //throws NotImplementedException here. wrong
            await t;

            t = Test3(true);
            await t; //throws InvalidOperationException here. correct
            t = Test3(false); //throws NotImplementedException here. wrong
            await t;

            t = Test4(true);
            await t; //throws InvalidOperationException here. correct
            t = Test4(false);
            await t; //throws NotImplementedException here. correct

            t = Test5(true);
            await t; //throws InvalidOperationException here. correct
            t = Test5(false);
            await t; //throws NotImplementedException here. correct
        }

        public static async Task<int> Test1(bool err) //CS1998: This async method lacks 'await'
        {
            if (err)
                throw new InvalidOperationException();
            return GetNum(42);
        }

        public static Task<int> Test2(bool err)
        {
            if (err)
                throw new InvalidOperationException();
            return Task.FromResult(GetNum(42));
        }

        public static Task<int> Test3(bool err)
        {
            if (err)
                return Task.FromException<int>(new InvalidOperationException());
            return Task.FromResult(GetNum(42));
        }

        public static async Task<int> Test4(bool err)
        {
            await Task.CompletedTask; // remove CS1998
            if (err)
                throw new InvalidOperationException();
            return GetNum(42);
        }

        public static Task<int> Test5(bool err)
        {
            try
            {
                if (err)
                    return Task.FromException<int>(new InvalidOperationException());
                return Task.FromResult(GetNum(42));
            }
            catch (Exception e)
            {
                return Task.FromException<int>(e);
            }
        }
        public static int GetNum(int num)
        {
            throw new NotImplementedException();
        }
    }
}

Test1 generate the warning I want to fix. Only Test4 and Test5 do not change program flow. But Test5 require a terrible amount of boilerplate code. Can it really be that the alternatives are to sprinkle "await Task.CompletedTask" all over my program or add crazy amount of silly Task.FromX code? At this point I really feel that CS1998 is plain wrong and must be silenced. Or am I missing something?

Edit: removing Task as result is not an option, it is typically an interface (that I cannot control) implementation.

Edit2: program flow is the key here. Changing a program to throw exceptions at different places can't be good. Imagine a program that create 3 tasks, shuts down a nuclear reactor, then Task.WaitAll(t1, t2, t3). Leaving the code as is or Test4\Test5, else the reactor explodes:-)

Edit3: I have often read that async create an unnecessary state machine (performance), but from my example you can see this is not quite true. Without this state machine, it will change program flow. Off course this must be in relation to a forced async interface implementation, else you would just make it synch, and I guess here CS1998 can be helpful:-) But CS1998 is not clever enough to understand if this is code you have control over or not. Tricky...

Edit4: I ended up with ignoring the warning, but the answer with the wrappers is a very good alternative. But I wish Microsoft could make a complementing "sync" keyword that could auto generate these wrappers in addition to generating a state machine if "async".

osexpert
  • 485
  • 1
  • 6
  • 18
  • 4
    Why would you want to declare a method as `async`, when it doesn't do anything asynchronous? – Johnathan Barclay Sep 25 '20 at 10:54
  • 2
    @JohnathanBarclay It could just be an interface you have to implement. – Wiktor Zychla Sep 25 '20 at 10:57
  • 1
    @WiktorZychla But here it isn't, and if it was then use `Task.FromResult`. – Johnathan Barclay Sep 25 '20 at 10:59
  • 1
    `Edit: removing Task as result is not an option, it is typically an interface (that I cannot control) implementation.` `Task.FromResult` then - and remove `async` from the function declaration. The key thing to understand is that the warning is, rightfully, pointing out that what you are doing is weird. Sometimes weird code is valid, sure, but _usually_ it is wrong. – mjwills Sep 25 '20 at 11:00
  • @osexpert: the number 4 is your best shot here. – Wiktor Zychla Sep 25 '20 at 11:01
  • Is `public static Task Test3(bool err) { return Task.Run(() => { if (err) throw new InvalidOperationException(); return Task.FromResult(GetNum(42)); }); }` an option? – mjwills Sep 25 '20 at 11:24
  • 1
    Does this answer your question? [Suppress warning CS1998: This async method lacks 'await'](https://stackoverflow.com/questions/13243975/suppress-warning-cs1998-this-async-method-lacks-await) –  Sep 25 '20 at 11:27
  • antispinwards: no, that question does not address changed program flow at all, which is my point here – osexpert Sep 25 '20 at 11:35
  • @osexpert - see the second answer there, with the pragma to disable the warning. At least, if you insist on using `async` purely for the effect of wrapping return values/exceptions in a `Task`, it's a possible approach. If you do this, you should leave a comment to document it so the next developer to come along knows the rationale for doing this. –  Sep 26 '20 at 20:23
  • @antispinwards Yes, I ended up with this, ignoring the error. Maybe I should answer my own question or resolve as duplicate. I feel the info that came in the answer (the wrapper) is still useful thou, as this seem to be the only real alternative to ignoring the error. – osexpert Sep 27 '20 at 21:45

1 Answers1

6

removing Task as result is not an option, it is typically an interface (that I cannot control) implementation

Using the async keyword creates a state machine, which is unnecessary if the implementation is synchronous.

If a method must return Task<TResult> to satisfy an interface contract, use Task.FromResult().

In the case of a method that returns a non-generic Task, return Task.CompletedTask.


In the case of exception handling, use Task.FromException:

public static Task<int> Test3(bool err)
{
    if (err) return Task.FromException<int>(new InvalidOperationException());
    
    try { return Task.FromResult(GetNum(42)); }
    catch (Exception e) { return Task.FromException<int>(e); }
}

If the amount of boilerplate is an issue then how about using these wrappers:

Task RunAsTask(Action action)
{
    try { action(); }
    catch (Exception e) { return Task.FromException(e); }
    return Task.CompletedTask;
}

Task<TResult> RunAsTask<TResult>(Func<TResult> func)
{
    try { return Task.FromResult(func()); }
    catch (Exception e) { return Task.FromException<TResult>(e); }
}

Then you can write your methods synchronously:

public static Task<int> Test1(bool err) => RunAsTask(() =>
{
    if (err)
        throw new InvalidOperationException();
    return GetNum(42);
});
Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
  • But as my exampel show, it will change how the program works and throw when getting the task instead of when the task is executed. Imagine shutting down a nuclear reactor: create 3 tasks, execute shutdown of reactor, Task.WhenAll(t1, t2, t3). After fixing the warnings, the reactor explodes:-) – osexpert Sep 25 '20 at 11:09
  • The wrapper is a nice alternative. Thanks. That at least eliminate Test5 as altarnative. Still torn between wrapper, disable CS1998 or add await Task.CompletedTask. Not really sure about pros\cons and the "right" choice. I have a feeling CS1998 is only about performance, but I guess that can be important too. – osexpert Sep 25 '20 at 11:43
  • 1
    `async` does degrade performance but that may not be an issue for you, however, I would say that using `async` purely for the purpose of cleaner exception handling is improper use of the concept. – Johnathan Barclay Sep 25 '20 at 11:57
  • It is possible that throwing when getting the task is ok most of the time, but since Task.FromException exist I believe this was not the intention. Coding this pattern by hand is error prone thou, and I actually can't remember seeing this pattern before either. IMO C# lack language "sugar" to cover this case, eg. a corresponding sync keyword that would generate this wrapper automatically (instead of a state machine when using async). – osexpert Sep 25 '20 at 12:23
  • Maybe, although implementing a synchronous, `Task`-returning method with complex exception handling probably isn't a common enough scenario to warrant language support. – Johnathan Barclay Sep 25 '20 at 12:28
  • IMHO Task.FromException in a synchronous method is pointless, just let the exception throw as normal. – Jeremy Lakeman Dec 17 '20 at 00:19
  • @JeremyLakeman It isn't pointless, and what you suggest changes the behaviour of an asynchronous method. `Task.FromException` has been developed _specifically for use within a synchronous method_. A method decorated with the `async` keyword will always place an exception on the `Task` rather than throwing it directly, and `Task.FromException` mimics this behaviour. That way, the exception is only thrown when the `Task` is awaited. – Johnathan Barclay Dec 17 '20 at 09:42