Table of Contents

CHT010: ValueTask captured in lambda/closure

Cause

A ValueTask or ValueTask<T> variable is captured from an outer scope into a lambda expression or a local function.

Rule Description

A ValueTask is a struct that can only be consumed once. When a ValueTask is captured in a closure (e.g., a lambda or a local function), it is potentially unsafe for two reasons:

  1. The closure might be invoked multiple times.
  2. The ValueTask may have been consumed by other code before the closure executes.

Awaiting a captured ValueTask leads to undefined behavior, and when backed by an IValueTaskSource, the second await may throw InvalidOperationException or return incorrect results.

How to Fix

Option 1: Convert to Task at declaration

If you need to use the result within a closure and ensure it can be accessed safely multiple times (or because the closure might be called multiple times), convert the ValueTask to a Task immediately:

// Before
ValueTask vt = GetValueTask();
var myLambda = async () => {
    // This is unsafe because 'vt' is a captured ValueTask
    return await vt; 
};

// After
Task t = GetValueTask().AsTask();
var myLambda = async () => {
    // This is safe because 't' is a Task
    return await t; 
};

Option 2: Use Preserve()

Use the Preserve() extension method for safe conversion. This is useful if you want to keep the ValueTask behavior but need to ensure it's safe to capture it for multiple consumes.

using CryptoHives.Foundation.Threading;

Task t = GetValueTask().Preserve();
var myLambda = async () => {
    // This is safe because .Preserve() allows multiple consumes
    return await t; 
};

Option 3: Store the result

If you only need the result of the ValueTask, await it once outside of the closure and store the resulting value:

// Before
ValueTask<int> vt = GetValueTask();
var myLambda = async () => {
    // This is unsafe
    return await vt; 
};

// After
int result = await GetValueTask().ConfigureAwait(false);
var myLambda = () => {
    // This is safe
    return result;
};

When to Suppress

Never suppress this diagnostic. Capturing a ValueTask in a closure without explicit preservation or conversion to a Task is a high-risk pattern that leads to undefined behavior.

Example

Violating Code

public async Task ProcessAsync()
{
    ValueTask vt = DoWorkAsync();
    
    // Captured vt in lambda
    var action = async () => {
        await vt; // CHT010: vt is captured and potentially already consumed
    };

    await action();
}

Fixed Code

public async Task ProcessAsync()
{
    Task t = DoWorkAsync().AsTask();
    
    // Captured t in lambda
    var action = async () => {
        await t; // OK - Task can be safely awaited multiple times
    };

    await action();
}

See Also