Introduction
Async/await syntax in C# and JavaScript simplifies asynchronous programming but introduces common confusions: blocking on async code (.Result/.Wait() causing deadlocks), forgetting to await an async call (fire-and-forget), misunderstanding what async actually does, and confusion about when code runs on which thread. The key insight is that async does not make code run on a background thread — it allows the current thread to be released during await and resumed later, enabling non-blocking I/O operations.
What async/await Actually Does
1// async does NOT create a new thread
2// It enables the method to "yield" control during await
3
4public async Task<string> FetchDataAsync()
5{
6 Console.WriteLine($"Before await: Thread {Thread.CurrentThread.ManagedThreadId}");
7
8 // This releases the current thread during the HTTP call
9 string result = await httpClient.GetStringAsync("https://api.example.com/data");
10
11 Console.WriteLine($"After await: Thread {Thread.CurrentThread.ManagedThreadId}");
12 // May be the same or different thread depending on SynchronizationContext
13
14 return result;
15}
16
17// Equivalent desugared version (simplified):
18public Task<string> FetchDataAsync_Desugared()
19{
20 var stateMachine = new FetchDataStateMachine();
21 stateMachine.builder = AsyncTaskMethodBuilder<string>.Create();
22 stateMachine.MoveNext();
23 return stateMachine.builder.Task;
24}
The compiler transforms async methods into state machines. When await is reached, the method returns a Task and the calling code continues. When the awaited operation completes, the state machine resumes from where it left off.
Confusion 1: Forgetting to Await
1// WRONG: Missing await — method runs but result is ignored
2public async Task ProcessAsync()
3{
4 SaveToDatabase(); // this is synchronous — fine
5 FetchDataAsync(); // WARNING: Task returned but not awaited!
6 // The fetch starts but the method continues without waiting for it
7 Console.WriteLine("Done"); // prints before fetch completes
8}
9
10// CORRECT: Await the async call
11public async Task ProcessAsync()
12{
13 await FetchDataAsync(); // waits for completion
14 Console.WriteLine("Done"); // prints after fetch completes
15}
16
17// Fire-and-forget (intentional — rare, requires care)
18public void StartBackgroundWork()
19{
20 _ = FetchDataAsync(); // discard operator — explicitly intentional
21 // Exceptions from FetchDataAsync are silently lost!
22}
1// Same issue in JavaScript
2async function process() {
3 fetchData(); // WRONG: promise not awaited
4 await fetchData(); // CORRECT: waits for completion
5}
Confusion 2: Blocking on Async Code
1// WRONG: .Result blocks the thread and can deadlock in ASP.NET/WPF
2public string GetData()
3{
4 string result = FetchDataAsync().Result; // BLOCKS — potential deadlock
5 return result;
6}
7
8// WRONG: .Wait() has the same problem
9public void ProcessData()
10{
11 FetchDataAsync().Wait(); // BLOCKS — potential deadlock
12}
13
14// CORRECT: async all the way
15public async Task<string> GetDataAsync()
16{
17 string result = await FetchDataAsync(); // non-blocking
18 return result;
19}
The deadlock occurs in environments with a SynchronizationContext (ASP.NET, WPF, WinForms). The .Result call blocks the context thread, and the await continuation needs that same thread to resume, creating a circular dependency.
Confusion 3: async void vs async Task
1// WRONG: async void — exceptions are unobservable
2public async void LoadData()
3{
4 var data = await FetchDataAsync(); // if this throws, the app crashes
5 Process(data);
6}
7
8// CORRECT: async Task — caller can await and catch exceptions
9public async Task LoadDataAsync()
10{
11 var data = await FetchDataAsync();
12 Process(data);
13}
14
15// Exception handling difference:
16try
17{
18 await LoadDataAsync(); // exception can be caught
19}
20catch (Exception ex)
21{
22 Console.WriteLine(ex.Message);
23}
24
25// async void exceptions cannot be caught by the caller
26// They go directly to the SynchronizationContext (crash in most cases)
async void should only be used for event handlers (e.g., button_Click). For all other cases, return Task or Task<T> so callers can await, catch exceptions, and compose operations.
Confusion 4: async Does Not Mean Parallel
1// Sequential — each call waits for the previous one
2public async Task SequentialAsync()
3{
4 var a = await FetchAAsync(); // waits for A
5 var b = await FetchBAsync(); // then waits for B
6 var c = await FetchCAsync(); // then waits for C
7 // Total time: A + B + C
8}
9
10// Parallel — all three start immediately
11public async Task ParallelAsync()
12{
13 var taskA = FetchAAsync(); // starts A
14 var taskB = FetchBAsync(); // starts B
15 var taskC = FetchCAsync(); // starts C
16
17 await Task.WhenAll(taskA, taskB, taskC); // wait for all
18 // Total time: max(A, B, C)
19
20 var a = taskA.Result; // safe after WhenAll
21 var b = taskB.Result;
22 var c = taskC.Result;
23}
1// JavaScript equivalent
2async function parallel() {
3 const [a, b, c] = await Promise.all([
4 fetchA(),
5 fetchB(),
6 fetchC(),
7 ]);
8}
Starting all tasks before awaiting them enables parallel execution. Task.WhenAll (C#) and Promise.all (JS) wait for all tasks to complete.
JavaScript-Specific Confusions
1// Confusion: async function always returns a Promise
2async function getData() {
3 return "hello"; // automatically wrapped in Promise.resolve("hello")
4}
5getData().then(value => console.log(value)); // "hello"
6
7// Confusion: await only works inside async functions
8function process() {
9 // const data = await fetchData(); // SyntaxError!
10}
11
12// Confusion: forEach does not await
13async function processItems(items) {
14 // WRONG: callbacks are not awaited
15 items.forEach(async (item) => {
16 await processItem(item); // these run concurrently, not sequentially
17 });
18
19 // CORRECT: use for...of for sequential
20 for (const item of items) {
21 await processItem(item);
22 }
23
24 // CORRECT: use Promise.all for parallel
25 await Promise.all(items.map(item => processItem(item)));
26}
Common Pitfalls
Using .Result or .Wait() on async methods: These block the calling thread and cause deadlocks in ASP.NET, WPF, and WinForms due to the SynchronizationContext. Use await instead. If you must call async from sync code, use Task.Run(() => method()).GetAwaiter().GetResult() as a last resort.
Using async void instead of async Task: async void methods cannot be awaited, and their exceptions crash the application. Only use async void for event handlers. All other async methods should return Task or Task<T>.
Awaiting inside forEach expecting sequential execution: In JavaScript, Array.forEach does not await async callbacks — all iterations start concurrently. Use for...of for sequential execution or Promise.all(array.map(...)) for intentional parallelism.
Making everything async unnecessarily: Adding async/await to a method that just returns another task adds overhead from the state machine. If a method only calls one async method and returns its result, return the task directly: return FetchDataAsync() instead of return await FetchDataAsync().
Not handling exceptions from Task.WhenAll: Task.WhenAll throws only the first exception. Other exceptions are silently swallowed unless you inspect each task's .Exception property individually. Always check all task results after WhenAll.
Summary
async does not create threads — it enables non-blocking waits with await
Always await async calls or explicitly discard with _ = MethodAsync()
Never use .Result or .Wait() — they cause deadlocks in UI and web contexts
Return Task or Task<T>, never async void (except for event handlers)
Start multiple tasks before awaiting for parallel execution with Task.WhenAll
In JavaScript, use for...of (not forEach) for sequential async iteration