Roslyn: Proposal: await? (Null-aware await)

Created on 2 Dec 2015  Â·  26Comments  Â·  Source: dotnet/roslyn

Currently you can't use await and null coalescing operator together, like

var result = await (obj as IFoo)?.FooAsync();

It would be nice if we could make await aware of nulls, like

var result = await? (obj as IFoo)?.FooAsync();

Instead of

var task = (obj as IFoo)?.FooAsync();
var result = task != null ? await task : null;

Or

var foo = obj as IFoo;
var result = foo != null ? await foo.FooAsync() : null;

Although, this can be done with pattern-matching

var result = obj is IFoo foo ? await foo.FooAsync() : null;

But still, it's too verbose for such a simple task; for await foo.Bar?.FAsync() this wouldn't apply though.

0 - Backlog Area-Language Design Feature Request

Most helpful comment

I like @bbarry's suggestion that await null should be a no-op (rather than throwing). It makes me thing of Console.WriteLine(null) which also works fine.

All 26 comments

I was thinking that this is already proposed, but didn't find anything.

I think this is more dangerous than valuable.
Awaiting a null throws a NullReferenceException that is extremely hard to track down. For example, it's very easy to have a non-async Task-returning method:

Task<string> GetNameAsync(int id)
{
    // ...
    return _cache.GetAsync(id);
}

That mistakenly returns null instead of a Task holding a null:

Task<string> GetNameAsync(int id)
{
    if (_cache.Contains(id))
    {
        return null;
    }

    // ...
    return _cache.GetAsync(id);
}

Enabling awaiting a null with await? will make the scenario of mistakenly awaiting a null with the regular await much more common.

This may be mitigated with having non-nullable reference types(#5032) as a prerequisite and restricting the usage of await only to these types leaving await? for nullable types.

:-1: on await?

How much of a breaking change would it be if await were changed to implicitly perform null skipping? That is:

var result = await (obj as IFoo)?.FooAsync();

wouldn't throw a null reference. Instead it would return the default value of the result type.

@i3arnon The point is not returning null from an async method, and #5032 woudn't help it, because we are using as and it will return a nullable anyway.

I'm thinking about a more generized solution for this; if forward pipe operator (#5445) works with await, meaning that the following is possible

var result = arg |> await Foo.BarAsync();

or with help of #5444, (from #4714 comment)

string result = await client.GetAsync("http://microsoft.com")
                |> await ::Content.ReadAsStringAsync();

Then, using a null-aware forward pipe operator, the example above could be written like this:

var result = obj as IFoo ?> await ::FooAsync();

or any other syntax.

The point is not returning null from an async method, and #5032 woudn't help it, because we are using as and it will return a nullable anyway.

@alrz of course you shouldn't return null from a task returning method, but it's possible and it happens. And when it happens it's a nasty bug that is extremely hard to find.
This is quite rare now as a regular async method can't return nulls so you wouldn't even try to await a null, but if you add await? then it will indeed become common to do so and you can easily use await instead of await?

Non-nullable reference types help because the compiler can enforce that await can only be used with these types and so there's no chance of a NRE. And if you do have a nullable type you must use await?.

@bbarry

How much of a breaking change would it be if await were changed to implicitly perform null skipping?

That would be confusing regardless of a breaking change. I liked @i3arnon suggestion to make await only work with non-nullables and use await? for nullables.

await nonNullableAwaitable;
await? nullableAwaitable;  // returns null
await nullableAwaitable!;  // throws
await! nullableAwaitble;   // might be better

By the way, #5032 needs more support in other places too, like #6563,

foreach(var item in nullableList) {}  // shouldn't work
foreach?(var item in nullableList) {} // use this, instead of
if(nullableList != null) foreach(var item in nullableList) {}  
foreach(var item in nullableList!) {} // note: if you prefer an exception
foreach!(var item in nullableList) {} // might be better

Or forward pipe operator (#5445),

nullable |> FuncitonTakingNonNullable(); // wouldn't work
nullable ?> FuncitonTakingNonNullable(); // use this, instead of
if(nullable != null) FuncitonTakingNonNullable(nullable);
FuncitonTakingNonNullable(nullable!);    // note: this might throw

What would be confusing about it?

Currently §7.7.6 states:

7.7.6.2 Classification of await expressions

The expression await t is classified the same way as the expression (t).GetAwaiter().GetResult(). Thus, if the return type of GetResult is void, the _await-expression_ is classified as nothing. If it has a non-void return type _T_, the _await-expression_ is classified as a value of type _T_.

7.7.6.3 Runtime evaluation of await expressions

At runtime, the expression await t is evaluated as follows:
• An awaiter _a_ is obtained by evaluating the expression (t).GetAwaiter().
• A bool _b_ is obtained by evaluating the expression (a).IsCompleted.
...
• Either immediately after (if _b_ was true), or upon later invocation of the resumption delegate (if _b_ was false), the expression (a).GetResult() is evaluated. If it returns a value, that value is the result of the _await-expression_. Otherwise the result is nothing.

The spec could be changed to avoid the null reference exception:

7.7.6.2 Classification of await expressions

The expression await t is classified the same way as the expression (t)?.GetAwaiter().GetResult() if the return type of GetResult is void or (t)?.GetAwaiter().GetResult() ?? default(T) where T is the return type of GetResult. If the return type of GetResult is void the _await-expression_ is classified as nothing. If it has a non-void return type _T_, the _await-expression_ is classified as a value of type _T_.

7.7.6.3 Runtime evaluation of await expressions

At runtime, the expression await t is evaluated as follows:
• An awaiter _a_ is obtained by evaluating the expression (t)?.GetAwaiter().
• A bool _b_ is obtained by evaluating the expression (a).IsCompleted != false.
...
• Either immediately after (if _b_ was true), or upon later invocation of the resumption delegate (if _b_ was false), the expression (a)?.GetResult() ?? default(T) is evaluated. If it returns a value, that value is the result of the _await-expression_. Otherwise the result is nothing.

As far as I can tell, the result would be exactly the same (aside from a state machine member type change and a few IL instructions) in all cases where t is not null, and not a runtime exception otherwise.

@bbarry The problem is with this part ?? default(T) That's why we don't have ref var declarations either (#6183). Quoting @gafter,

because it is too magical giving local variables initial values out of thin air.

Same would be true for await on nulls. On the other hand, with await?, await! etc, we don't assume anything about user intent — what if an exception is preferred? (see my updated comment above).

also Quoting @gafter (https://github.com/dotnet/roslyn/issues/6400#issuecomment-152256592):

I think adding a language construct that implicitly throws NullReferenceException in new and likely scenarios is not what we would want to do.

await currently throws NullReferenceException implicitly in what I would argue is a reasonably likely scenario and I think consistency to the language of today is better than consistency to a language that hasn't yet been specified. A future feature may need a more detailed spec change, but I think that is still preferable than a stand alone spec change to partially implement some future feature.

@bbarry I don't know what is the difference between _implicitly_ throwing NullReferenceException or _implicitly_ not throwing it. The point is that it shouldn't be implicit.

This might be complicated on the compiler side but it might be a nice solution if ?. could look to the right hand side of the expression, and return a completed Task if an async method is called on null. The await would then just do its normal thing.

Ie instead of returning default(Task) it should return Task.FromResult<TResult>(default(TResult)).

What would it be if await?ing a Task<TStruct> where `TStruct' is value type? Compiler error?

@diryboy I think this is related to #5032 whereas await? only makes sense for a nullable Task (or any other awaitable) since Task<TStruct>? still can be null in that case, and the result will be a TStruct?. That said, to be able to use await? on a generic Task<T>? it must be known at compile-time that T is a class or struct because as it currently specified T? is quite different when T is a reference type or it is a value type.

@alrz Ok, so my question is not applicable here actually.

?? is the null coalescing operator

I like @bbarry's suggestion that await null should be a no-op (rather than throwing). It makes me thing of Console.WriteLine(null) which also works fine.

It might not be as pretty and I'm all for the nullable await but we need the a GetValueOrDefault() just like for nullable.

Here's an extension method that works if anyone is interested.

//Extension method
 public static Task<T> GetValueOrDefault<T>(this Task<T> task, T defaultValue = default(T))
            => task == null ? defaultValue : task;

//To use

 public async Task<int> SaveChangesAsync() 
            => await (entities?.SaveChangesAsync()).GetValueOrDefault(100);

//or

 public async Task<int> SaveChangesAsync() 
            => await (entities?.SaveChangesAsync()).GetValueOrDefault();

I've tested it and this works and it's easy enough for me to stick with.

One case where I've often wished for something like await? is when getting the content of an HTTP response:

await? httpResponseMessage.Content?.ReadAsStringAsync()

@ljw1004 @bbarry

await null should be a no-op (rather than throwing)

I think this will work out especially with explicitly nullable reference types,

object result = await obj.DoAsync();
object? result = await obj?.DoAsync();

We are now taking language feature discussion on https://github.com/dotnet/csharplang for C# specific issues, https://github.com/dotnet/vblang for VB-specific features, and https://github.com/dotnet/csharplang for features that affect both languages.

See also https://github.com/dotnet/csharplang/issues/35 for a proposal under consideration.

why this proposal got rejected? its perfect use case is for null-able types.

@MkazemAkhgary

What makes you think the proposal was rejected? Read gafter's last comment.

@MkazemAkhgary, read the last message from @gafter.

There's an open issue on the C# language repo for this: https://github.com/dotnet/csharplang/issues/35

@jnm2 @paulomorgado I see, I should've noticed more carefully, thanks a lot.

Innocent

await x.CompletedAction?.CallAsync(provider_, id, data);

NO!!!
WEIRD ERROR MESSAGE.

ugly fix
` await (x.CompletedAction?.CallAsync(provider_, id, data) ?? Task.CompletedTask);

Was this page helpful?
0 / 5 - 0 ratings