Why doesn't an async LINQ Select lambda require a return value
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
An async lambda used with LINQ Select does require a return value, but the value might be implicit in expression-bodied syntax. The confusion usually comes from how the compiler infers delegate types for async lambdas. Select always performs projection, so each source element must map to some result type, often Task<T> in async scenarios.
Select Always Projects
Enumerable.Select expects a function from input to output. With async lambdas, that output is usually a task.
The lambda clearly returns a value, and because it is async, the compiler wraps it into Task<int>.
Why It Looks Like No Return Is Needed
Expression-bodied async lambdas can hide explicit return.
This still returns Task<int> if ComputeAsync returns Task<int>. The return is implicit.
Another valid pattern returns non-generic Task when no result value is needed.
Projection still occurs. The projected type is Task instead of Task<T>.
Compiler Inference and Delegate Types
The compiler chooses a delegate type based on context.
For Select:
- Source type
IEnumerable<TSource>. - Selector type
Func<TSource, TResult>.
If selector body is async with return int, then TResult becomes Task<int>. If selector body has no value return, TResult becomes Task.
That is why "no return" seems possible. It is actually a return of a task type.
Common Correct Patterns
Pattern A, concurrent async projection:
Pattern B, sequential async processing:
Use sequential flow when order and backpressure matter.
Pattern C, bounded concurrency:
This avoids flooding external systems.
Avoid async void Traps
List<T>.ForEach expects Action<T>, not task-returning delegates. Passing async lambdas there creates async void, which breaks composition and exception handling.
Prefer Select with Task.WhenAll, or plain foreach with await.
Deferred Execution Gotcha
LINQ query operators are deferred. Task creation may not start until you enumerate.
If you need immediate materialization, call ToList before passing tasks onward.
SelectMany Is Different from Select
Some confusion comes from mixing projection operators. Select maps one input to one output item or task, while SelectMany flattens nested sequences. In async code, choose Select for task projection and flatten only after awaited completion when needed.
Understanding this distinction makes async projection code easier to read and avoids type-inference confusion.
Common Pitfalls
- Assuming async
Selectremoves need for projection return types. - Confusing implicit expression-body return with no return.
- Using
List.ForEachasync lambdas and losing exception flow. - Forgetting
Task.WhenAlland leaving tasks unobserved. - Ignoring deferred execution when debugging start timing.
Summary
- Async lambdas in
Selectstill return projected values, usually tasks. - Expression-bodied lambdas can hide explicit
returnsyntax. - Use
Task.WhenAllfor concurrent async projection. - Use sequential loops when order or backpressure is required.
- Avoid
async voidpatterns in collection iteration APIs.

