How does adding a break in a while loop resolve overload ambiguity?

It's easiest to just pull out async as well as the lambdas here, as it emphasizes what's going on. Both of these methods are valid and will compile:

public static void Foo()
{
    while (true) { }
}
public static Action Foo()
{
    while (true) { }
}

However, for these two methods:

public static void Foo()
{
    while (true) { break; }
}
public static Action Foo()
{
    while (true) { break; }
}

The first compiles, and the second does not. It has a code path that doesn't return a valid value.

In fact, while(true){} (along with throw new Exception();) is an interesting statement in that it is the valid body of a method with any return type.

Since the infinite loop is a suitable candidate for both overloads, and neither overload is "better", it results in an ambiguity error. The non-infinite loop implementation only has one suitable candidate in overload resolution, so it compiles.

Of course, to bring async back into play, it is actually relevant in one way here. For the async methods they both always return something, whether it's a Task or a Task<T>. The "betterness" algorithms for overload resolution will prefer delegates that return a value over void delegates when there is a lambda that could match either, however in your case the two overload both have delegates that return a value, the fact that for async methods returning a Task instead of a Task<T> is the conceptual equivalent of not returning a value isn't incorporated into that betterness algorithm. Because of this the non-async equivalent wouldn't result in an ambiguity error, even though both overloads are applicable.

Of course it's worth noting that writing a program to determine if an arbitrary block of code will ever complete is a famously unsolvable problem, however, while the compiler cannot correctly evaluate whether every single snippet will complete, it can prove, in certain simple cases such as this one, that the code will in fact never complete. Because of this there are ways of writing code that will clearly (to you and me) never complete, but that the compiler will treat as possibly completing.


Leaving async out of this to start with...

With the break, the end of the lambda expression is reachable, therefore the return type of the lambda has to be void.

Without the break, the end of the lambda expression is unreachable, so any return type would be valid. For example, this is fine:

Func<string> foo = () => { while(true); };

whereas this isn't:

Func<string> foo = () => { while(true) { break; } };

So without the break, the lambda expression would be convertible to any delegate type with a single parameter. With the break, the lambda expression is only convertible to a delegate type with a single parameter and a return type of void.

Add the async part and void becomes void or Task, vs void, Task or Task<T> for any T where previously you could have any return type. For example:

// Valid
Func<Task<string>> foo = async () => { while(true); };
// Invalid (it doesn't actually return a string)
Func<Task<string>> foo = async () => { while(true) { break; } };
// Valid
Func<Task> foo = async () => { while(true) { break; } };
// Valid
Action foo = async () => { while(true) { break; } };