Chapter 05 Writing Asynchronous Code
4 min read- Chapter 05 — Writing asynchronous code
- 5.1 Introducing asynchronous functions
- 5.1.1 First encounters
- 5.1.2 Breaking down the first example (what matters)
- 5.2 Thinking about asynchrony
- 5.2.1 Fundamentals of async execution
- 5.2.2 Synchronization contexts (why you should care)
- 5.2.3 Modeling async methods
- 5.3 Async method declarations
- 5.3.1 Return types
- 5.3.2 Parameters
- 5.4 Await expressions
- 5.4.1 The awaitable pattern
- 5.4.2 Restrictions
- 5.5 Wrapping of return values
- 5.6 Asynchronous method flow
- 5.6.1 What is awaited and when?
- 5.6.2 Evaluation of await expressions
- 5.6.4 Exception unwrapping
- 5.6.5 Method completion
- 5.7 Asynchronous anonymous functions
- 5.8 Writing asynchronous code (performance + API design)
- 5.8.1 The “most common” case: `ValueTask
` - 5.8.2 Custom task types (rare)
- 5.9 Async `Main` methods (C# 7.1+)
Chapter 05 — Writing asynchronous code
Purpose of this chapter: build a correct mental model for async/await so you can reason about control flow, exceptions, cancellation, context capture, and performance tradeoffs in production services.
5.1 Introducing asynchronous functions
Async is fundamentally about not blocking a thread while waiting (usually for I/O).
What to be able to say in interviews:
- Async improves throughput/scalability for I/O-bound workloads.
- Async doesn’t automatically make CPU-bound code faster (it may even add overhead).
5.1.1 First encounters
The “shape” you should recognize:
- An async method returns quickly with a
Task/Task<T>representing future completion. - Awaiting the task asynchronously pauses the method and resumes later.
5.1.2 Breaking down the first example (what matters)
Interview-ready checklist for a typical async call:
- Use
awaitinstead of.Result/.Wait(). - Pass
CancellationTokenthrough the call chain. - Avoid unbounded parallelism; use throttling for fan-out.
5.2 Thinking about asynchrony
5.2.1 Fundamentals of async execution
Key points:
awaitdoes not create a new thread by itself.- Continuations run later; the scheduler/context decides where.
- Your code becomes a state machine under the hood (chapter 6).
5.2.2 Synchronization contexts (why you should care)
Context capture influences where continuations run:
- UI apps often require resuming on the UI thread.
- In server code, capturing context can be unnecessary overhead.
Senior answer:
- Know when
ConfigureAwait(false)is appropriate (typically library code), but avoid cargo-culting it; understand the environment and correctness requirements.
5.2.3 Modeling async methods
Think of an async method as:
- “Do synchronous work until the first incomplete await; then return a task and resume later.”
5.3 Async method declarations
5.3.1 Return types
Common return types:
Task/Task<T>: the standard for async work.ValueTask<T>: for scenarios where results are often already available (reduce allocations) but comes with constraints (avoid multiple awaits unless you know the source supports it).
Rule of thumb:
- Prefer
Task/Task<T>unless profiling shows allocation pressure and your scenario fitsValueTask<T>.
5.3.2 Parameters
Common patterns:
- Always accept/pass
CancellationTokenfor cancellable work. - Avoid
async voidexcept for event handlers (because errors can’t be awaited/observed reliably).
5.4 Await expressions
5.4.1 The awaitable pattern
Interviews sometimes go here: await works on types that follow an “awaitable” pattern (not only Task).
Practical takeaway:
- Most production code uses
Task/ValueTask, but knowing the concept helps explain why “custom task-like types” can exist.
5.4.2 Restrictions
The important restriction for day-to-day work:
- You can only
awaitin anasyncmethod (or contexts that support it, likeawait foreachlater).
5.5 Wrapping of return values
Mental model:
return value;in anasync Task<T>method completes the returned task with that value.return;in anasync Taskmethod completes the task.
Interview trap:
- Exceptions thrown inside async methods are captured into the task and are observed when awaited (or via task observation APIs).
5.6 Asynchronous method flow
5.6.1 What is awaited and when?
Only incomplete awaits suspend execution.
Practical implications:
- If the awaited task is already complete, the method may continue synchronously (which can matter for recursion or reentrancy reasoning).
5.6.2 Evaluation of await expressions
Be clear on ordering:
- Expressions are evaluated before the await suspends.
- Side effects happen before suspension.
5.6.4 Exception unwrapping
What to explain:
awaitrethrows the original exception (preserving a useful stack trace shape), rather than forcing consumers to manually unwrapAggregateException(common when blocking on tasks).
5.6.5 Method completion
Completion occurs when:
- The method reaches the end, returns, or throws.
- The returned task transitions to RanToCompletion/Faulted/Canceled accordingly.
5.7 Asynchronous anonymous functions
Async lambdas are common:
Func<Task<int>> work = async () => { await Task.Delay(10); return 42; };
Gotcha:
- Don’t use
asyncin LINQ operators that expect synchronous delegates unless you understand you’re producing tasks (e.g.,Select(async x => ...)gives youIEnumerable<Task<T>>).
5.8 Writing asynchronous code (performance + API design)
5.8.1 The “most common” case: ValueTask<TResult>
Use ValueTask<T> when:
- You frequently complete synchronously and want to avoid task allocations.
Costs/risks:
- More complex usage rules; can be misused by awaiting multiple times or by storing it.
5.8.2 Custom task types (rare)
Know the idea:
- You can build task-like types for specialized schedulers/perf needs, but it’s advanced and easy to get wrong.
Senior guidance:
- Prefer proven primitives and measure first.
5.9 Async Main methods (C# 7.1+)
static async Task Main(...) is a convenience that makes console app startup async without manual GetAwaiter().GetResult() plumbing.