Chapter 05 Writing Asynchronous Code

4 min read
Rapid overview

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 await instead of .Result/.Wait().
  • Pass CancellationToken through 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:

  • await does 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 fits ValueTask<T>.

5.3.2 Parameters

Common patterns:

  • Always accept/pass CancellationToken for cancellable work.
  • Avoid async void except 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 await in an async method (or contexts that support it, like await foreach later).

5.5 Wrapping of return values

Mental model:

  • return value; in an async Task<T> method completes the returned task with that value.
  • return; in an async Task method 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:

  • await rethrows the original exception (preserving a useful stack trace shape), rather than forcing consumers to manually unwrap AggregateException (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 async in LINQ operators that expect synchronous delegates unless you understand you’re producing tasks (e.g., Select(async x => ...) gives you IEnumerable<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.