DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

5 Advanced Async Concepts That Make Sense Once You Understand the Runtime

5 Advanced Async Concepts That Make Sense Once You Understand the Runtime

5 Advanced Async Concepts That Make Sense Once You Understand the Runtime

Why TaskCompletionSource, custom awaiters, and continuations behave
the way they do
\
Cristian Sifuentes\
February 23, 2026


Async/await feels simple --- until it doesn't.

Most developers are comfortable writing:

await service.GetDataAsync();
Enter fullscreen mode Exit fullscreen mode

But once you start building libraries, infrastructure, or
high‑throughput systems, you inevitably encounter APIs that feel sharp:

  • TaskCompletionSource<T>
  • ValueTask
  • ManualResetValueTaskSourceCore<T>
  • Custom awaiters
  • RunContinuationsAsynchronously

They look low-level because they are.

And they only make sense when you stop thinking about async as syntax
and start thinking about it as a runtime model.

This article breaks down five advanced async concepts --- not from a
"how to use" angle, but from a "why the runtime behaves this way"
perspective.


1. TaskCompletionSource<T>{=html} --- State Machines and Races

TaskCompletionSource<T> (TCS) exists to bridge non-task APIs into the
Task world.

At face value, it's simple:

var tcs = new TaskCompletionSource<int>();

DoWork(
    onSuccess: value => tcs.SetResult(value),
    onFailure: ex => tcs.SetException(ex)
);

return tcs.Task;
Enter fullscreen mode Exit fullscreen mode

But under the hood, TCS is a single-transition state machine.

It can complete once. Only once.

Calling:

tcs.SetResult(42);
tcs.SetResult(43);
Enter fullscreen mode Exit fullscreen mode

throws InvalidOperationException.

The real problem isn't developers calling it twice intentionally. It's
race conditions.

Timeout handlers racing with success callbacks.\
Cancellation tokens racing with event completions.

That's why production-grade code uses:

tcs.TrySetResult(value);
tcs.TrySetCanceled();
Enter fullscreen mode Exit fullscreen mode

The runtime does not serialize your concurrency. You are responsible for
that coordination.

Hidden Detail: Inline Continuations

By default, calling SetResult may execute continuations synchronously.

That means:

  • Arbitrary user code runs immediately.
  • It runs on your thread.
  • It can reenter your code.

That behavior is fast --- and dangerous.

More on that shortly.


2. ManualResetValueTaskSourceCore<T>{=html} --- Allocation-Free Async

ValueTask exists to reduce allocations.

But a ValueTask must reference something. When it's not already
completed, it typically points to an IValueTaskSource<T>
implementation.

ManualResetValueTaskSourceCore<T> is the primitive that enables
reusable async operations.

Example:

private ManualResetValueTaskSourceCore<int> _core;

public ValueTask<int> ReadAsync()
{
    _core.Reset();
    StartOperation();
    return new ValueTask<int>(this, _core.Version);
}
Enter fullscreen mode Exit fullscreen mode

This introduces a critical idea: version tokens.

Each reset increments a version. Awaiters capture that version. If the
awaiter is reused incorrectly, the runtime throws.

This is deliberate.

Reusable awaitables trade safety for performance.\
The runtime installs tripwires so misuse fails fast instead of
corrupting memory silently.

At scale, allocating a Task per operation is too expensive. So the
runtime gives you the tools --- but not the guardrails.


3. Custom Awaiters --- await Is Pattern-Based

await is not magic syntax. It is pattern recognition.

When the compiler sees:

await something;
Enter fullscreen mode Exit fullscreen mode

It looks for:

  • GetAwaiter()
  • IsCompleted
  • OnCompleted / UnsafeOnCompleted
  • GetResult()

Anything implementing that contract becomes awaitable.

This means async behavior is not defined by the await keyword --- it's
defined by how your awaiter schedules continuations.

The awaiter decides:

  • Should continuation run synchronously?
  • On what scheduler?
  • On what thread?
  • Should it capture a context?

At this level, async is just structured continuation registration.


4. Building Your Own Awaitable

A minimal awaiter looks like this:

public readonly struct SimpleAwaiter : INotifyCompletion
{
    public bool IsCompleted => false;

    public void OnCompleted(Action continuation)
    {
        ThreadPool.QueueUserWorkItem(_ => continuation());
    }

    public void GetResult() { }
}
Enter fullscreen mode Exit fullscreen mode

And the awaitable:

public readonly struct SimpleAwaitable
{
    public SimpleAwaiter GetAwaiter() => new SimpleAwaiter();
}
Enter fullscreen mode Exit fullscreen mode

The runtime does not care about semantics --- only protocol compliance.

This is why mistakes here are subtle and catastrophic:

  • Resuming synchronously when you shouldn't
  • Capturing context unintentionally
  • Creating deep reentrancy chains
  • Triggering stack explosions

Once you implement an awaiter, you are not consuming async.

You are participating in the scheduler.


5. RunContinuationsAsynchronously --- Choosing Isolation

By default, completing a Task may run continuations inline:

tcs.SetResult(42);
Enter fullscreen mode Exit fullscreen mode

Inline continuation execution means:

  • Your thread executes user code.
  • Deep call stacks can form.
  • Reentrancy becomes likely.

This is why TCS supports:

new TaskCompletionSource<int>(
    TaskCreationOptions.RunContinuationsAsynchronously
);
Enter fullscreen mode Exit fullscreen mode

With this flag:

  • Continuations are queued.
  • Execution becomes predictable.
  • Isolation increases.

You pay a scheduling cost.\
You gain safety and clarity.

In high-throughput systems, inline execution may be desirable.\
In library code, it is almost always a liability.

Continuation scheduling is a design decision --- not an implementation
detail.


Final Thoughts

At scale, async stops being convenience syntax and becomes a concurrency
contract.

Tasks are not promises.\
Awaiters are not helpers.\
Continuations are real code executing somewhere, sometime, on some
thread.

When you understand the runtime model, these APIs stop feeling sharp.

They start feeling precise.

And precision is what high-performance systems require.


If this helped you reason about async at a deeper level, consider
sharing it with someone building infrastructure, not just features.

Top comments (0)