CA2012: How to Use ValueTask Correctly in .NET
.Result before it completes, or discarding it entirely. The fix is almost always the same: await it once, directly, at the call site. If you need to await the result multiple times, call .AsTask() first and use the returned Task.CA2012 is a .NET Roslyn analyser rule in the Reliability category. It analyses your code at compile time and flags any call site where a ValueTask is consumed in a way that breaks its one-shot contract. ValueTask was introduced as a performance optimisation - a way to avoid heap allocations on hot paths where an async operation frequently completes synchronously. But that optimisation comes with strict rules. Break them and you get exceptions, data corruption, or subtle performance regressions that are hard to reproduce.
This article walks through every misuse pattern CA2012 detects, explains why each one is dangerous, and shows the correct alternative.
Why does ValueTask exist?
Every Task or Task<T> is a reference type - it lives on the heap. For most code that is fine. But imagine an interface method like IValueTaskSource<T> that is implemented by a high-throughput cache or a socket read loop. If 99% of calls hit the cache synchronously, you are allocating a Task object for every call just to return an already-known value.
ValueTask<T> is a struct that can hold either a synchronous result or a pointer to an underlying Task. In the synchronous case, no heap object is created. The trade-off is that ValueTask is a one-shot value - it can only be consumed once and only in specific ways. Task is a shared object; ValueTask is a receipt you hand in once and throw away.
The ValueTask consumption contract
Microsoft documents three rules for consuming a ValueTask. Violating any of them is undefined behaviour:
- Await it at most once. The underlying value source may be recycled from a pool after the first await. A second await could read stale or recycled data.
- Do not call
.GetAwaiter().GetResult()(or.Result) unless you know it has already completed. Blocking on an incompleteValueTaskis not supported and may deadlock or throw. - Do not discard it without observing it. Unlike a
Task, a discardedValueTaskcannot be observed through the finaliser - you lose any exception silently.
ValueTask like a single-use voucher. Once redeemed (awaited), it is gone. You can convert it to a reusable Task with .AsTask() before the first await if you need to use it multiple times.What CA2012 catches
CA2012 (Use ValueTasks correctly) is in the Reliability category and is enabled by default as a suggestion from .NET 10 onward. It detects the following patterns at the call site - that is, where the ValueTask is returned from a method and immediately misused.
Pattern 1: Awaiting the same ValueTask twice
This is the most common violation. A ValueTask is stored in a local variable and then awaited more than once.
// ❌ CA2012 violation - double await
public async Task UseValueTaskIncorrectlyAsync()
{
ValueTask<int> task = GetNumberAsync();
int first = await task; // first await - ok
int second = await task; // second await - undefined behaviour
}
// ✅ Correct - call the method twice
public async Task UseValueTaskCorrectlyAsync()
{
int first = await GetNumberAsync();
int second = await GetNumberAsync();
}The reason the second await is unsafe: GetNumberAsync might be backed by a pooled IValueTaskSource. After the first await, the pool may have already reclaimed and reused that source object. The second await reads whatever data happens to be in the recycled slot.
Pattern 2: Storing a ValueTask and awaiting it later
Storing a ValueTask in a field or returning it while also keeping a reference is equally dangerous - the underlying source may have been recycled by the time the stored value is awaited.
// ❌ CA2012 violation - stored in a field, used later
private ValueTask<int> _pending;
public void Start()
{
_pending = GetNumberAsync(); // stored, not immediately awaited
}
public async Task<int> GetResultAsync()
{
return await _pending; // awaited much later - unsafe
}
// ✅ Correct - convert to Task if you need to store it
private Task<int> _pending;
public void Start()
{
_pending = GetNumberAsync().AsTask(); // Task is safe to store and await multiple times
}
public async Task<int> GetResultAsync()
{
return await _pending;
}Pattern 3: Accessing .Result before completion is confirmed
Unlike Task, which you can safely poll with IsCompleted and then read .Result synchronously in a fast path, doing the same on a ValueTask is only safe if you have obtained the ValueTask and confirmed IsCompleted on the same instance without any intervening suspension points.
// ❌ Risky - reading .Result without confirming completion first
ValueTask<int> vt = GetNumberAsync();
int value = vt.GetAwaiter().GetResult(); // may block or throw
// ✅ Correct - check IsCompleted first (synchronous fast path)
ValueTask<int> vt = GetNumberAsync();
if (vt.IsCompleted)
{
int value = vt.GetAwaiter().GetResult(); // safe: already done
}
else
{
int value = await vt; // still awaited at most once
}Pattern 4: Discarding a ValueTask without observing it
Calling a method that returns a ValueTask and ignoring the return value means you will never see any exception the operation throws. With Task this is also bad practice, but the runtime can surface the exception via the TaskScheduler.UnobservedTaskException event. With a discarded ValueTask there is no such safety net.
// ❌ CA2012 violation - ValueTask discarded GetNumberAsync(); // return value ignored entirely // ✅ Correct - await it await GetNumberAsync(); // ✅ Also acceptable - if you genuinely want fire-and-forget, // convert to Task and discard that (you still lose the exception, // but at least the Task-based safety net may catch it) _ = GetNumberAsync().AsTask();
The AsTask() escape hatch
If you have a legitimate reason to await the same result multiple times - for example, you are fanning out the same result to multiple consumers - call .AsTask() immediately after obtaining the ValueTask. This converts it into a regular Task, which is safe to share and await as many times as you like.
ValueTask<int> numberValueTask = GetNumberAsync(); // Convert once, then use the Task freely Task<int> numberTask = numberValueTask.AsTask(); int first = await numberTask; // fine int second = await numberTask; // also fine - Task is reusable
.AsTask() once per ValueTask instance. The same rule applies - you get one conversion. After that, use the returned Task however you like.How to enable and configure CA2012
From .NET 10 onward, CA2012 is enabled as a suggestion by default. In earlier projects you may need to enable it explicitly.
Raise severity to a warning or error
# .editorconfig
[*.{cs,vb}]
dotnet_diagnostic.CA2012.severity = warning # or errorEnable the full reliability rule set
# .editorconfig dotnet_analyzer_diagnostic.category-Reliability.severity = warning
When to suppress CA2012
Suppression is only appropriate when you both wrote the method being called and know for certain that its ValueTask always wraps a plain Task (never a pooled IValueTaskSource). In that case the double-await or store-and-await pattern is technically safe, though still bad practice.
#pragma warning disable CA2012 // Suppress for a single line only when you own the callee // and you know it never uses IValueTaskSource pooling ValueTask<int> vt = MyKnownSafeMethod(); int a = await vt; int b = await vt; #pragma warning restore CA2012
In the vast majority of cases, just fix the code. The suppression exists as an escape hatch, not a habit.
Summary of violations and fixes
| Violation | Risk | Fix |
|---|---|---|
Await the same ValueTask twice | Undefined behaviour, data corruption | Call the method twice, or convert with .AsTask() |
Store ValueTask in a field | Source may be recycled before second use | Store .AsTask() instead |
Call .GetResult() without confirming completion | Deadlock or exception | Check .IsCompleted first, or just await |
| Discard without observing | Silent exception loss | Always await, or at minimum _ = vt.AsTask() |
Frequently asked questions
From .NET 10 it is enabled as a suggestion. In earlier SDK versions it may not be active. You can enable it explicitly in .editorconfig by setting dotnet_diagnostic.CA2012.severity to warning or error.
Task is a class (reference type) and its completed state and result are stored on the heap indefinitely. ValueTask is a struct that may wrap a pooled IValueTaskSource - after the first await, the pool can reclaim and reuse that source object. A second await could read stale recycled data.
Use AsTask() when calling the method a second time would repeat work you do not want repeated - for example, if the method performs I/O or modifies state. If the operation is idempotent (a pure cache lookup, for instance), calling it twice is cleaner and avoids the Task allocation.
Yes. The rule applies to both ValueTask and ValueTask<T>. Both have the same one-shot consumption contract.
Yes, but you should not do this globally. CA2012 catches real bugs. If you find yourself suppressing it broadly, it is a sign that ValueTask is being misused throughout the codebase and the underlying code should be fixed instead.
No. Task is the right default. ValueTask is an optimisation for hot paths where the operation frequently completes synchronously and allocation pressure is measurable. Use Task unless profiling shows a real benefit from switching.