Async call for delegate in cycle
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
Invoking async delegates inside loops in C# requires careful handling of closures, concurrency, and exception propagation. The common pattern is iterating over a collection and calling an async delegate (like Func<T, Task>) for each item. The main decision is whether to run delegates sequentially (one at a time) or concurrently (all at once). Getting this wrong causes bugs like captured loop variables, swallowed exceptions, and race conditions. This article covers both patterns with correct implementations.
The Problem: Async Delegate in a Loop
This is the simplest correct pattern. foreach with await runs each delegate invocation sequentially. The loop does not advance until the current Task completes.
Sequential Execution
Sequential execution is correct when order matters (database inserts with dependencies) or when the downstream service cannot handle concurrent requests.
Concurrent Execution with Task.WhenAll
Task.WhenAll starts all delegates immediately and waits for all to complete. This is faster when operations are independent (API calls, file uploads).
Controlling Concurrency with SemaphoreSlim
SemaphoreSlim limits how many delegates run at the same time, preventing resource exhaustion when processing large collections.
Closure Bug with Loop Variables
In C# 5+, foreach captures the loop variable per iteration. With for loops, the index variable is shared across iterations and must be copied to a local variable.
Exception Handling
Task.WhenAll only throws the first exception. Inspect individual Task.Exception properties to get all failures.
Common Pitfalls
- Fire-and-forget in loops: Calling
processAsync(item)withoutawaitin aforeachloop starts all tasks but does not wait for any. Exceptions are silently lost and the method returns before processing finishes. - Capturing loop variable by reference: In
forloops, the indexiis shared across iterations. Closures that captureisee the final value unless you copy to a local variable. - Task.WhenAll swallowing exceptions:
await Task.WhenAll(tasks)throws only the first exception. If multiple tasks fail, you must inspect each task'sExceptionproperty to find all errors. - Unbounded concurrency:
Task.WhenAllwith thousands of tasks can exhaust thread pool, connections, or memory. UseSemaphoreSlimto cap concurrency when processing large collections. - Mixing sync and async delegates: If the delegate is
Action<T>instead ofFunc<T, Task>, async lambdas becomeasync void, which cannot be awaited and crash the process on unhandled exceptions.
Summary
- Use
foreachwithawaitfor sequential async delegate execution - Use
Task.WhenAll(items.Select(x => delegateAsync(x)))for concurrent execution - Limit concurrency with
SemaphoreSlimto prevent resource exhaustion - Always use
Func<T, Task>(notAction<T>) for async delegates to enable proper awaiting - Copy
forloop variables to local variables to avoid closure capture bugs - Inspect individual
Task.Exceptionproperties afterTask.WhenAllto find all failures
Related reading
- Async call in ember testing
- Async callback function in jquery .done not executing
- Async, Callbacks, and OOP JavaScript how do I organize this?
- Async communication between spring boot micro services
- Async Concurrent Queue with max concurrency
- async Elixir vs async Julia
- Async event handlers with stateful event args
- async function in Python not working as expected
.png&w=3840&q=75)
Tackling System Design Interview Problems
A short course that equips you with the skills to approach system design interviews methodically.
Start the free courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.