Table of Contents

CHT008: ValueTask not awaited or consumed

Cause

A ValueTask or ValueTask<T> is returned from a method call but is never awaited, stored, or otherwise consumed.

Rule Description

When a ValueTask is returned but not consumed:

  1. Resource leaks: When backed by pooled IValueTaskSource, the pooled object is never returned to the pool
  2. Silent failures: Exceptions thrown by the async operation are lost
  3. Unintended behavior: The operation may not complete as expected
  4. Pool exhaustion: Repeated leaks can exhaust the object pool

This is particularly important for CryptoHives.Foundation.Threading primitives which use pooled IValueTaskSource implementations.

How to Fix

Option 1: Await the ValueTask

// Before
GetValueTask(); // Warning: not consumed

// After
await GetValueTask();

Option 2: Store for later consumption

// Before
GetValueTask(); // Warning

// After
ValueTask vt = GetValueTask();
// ... later ...
await vt;

Option 3: Explicitly discard

If you intentionally don't want to await:

// Before
GetValueTask(); // Warning

// After
_ = GetValueTask(); // Explicit discard - warning suppressed

Option 4: Use discard with await for fire-and-forget

// Fire and forget with exception handling
_ = Task.Run(async () =>
{
    try
    {
        await GetValueTask();
    }
    catch (Exception ex)
    {
        // Log the exception
    }
});

When to Suppress

Only suppress if you understand the consequences:

#pragma warning disable CHT008
GetValueTask(); // Intentionally not awaited
#pragma warning restore CHT008

However, prefer explicit discard (_ =) over suppression as it makes intent clear.

Example

Violating Code

public void ProcessData()
{
    // CHT008: ValueTask not consumed
    _asyncEvent.WaitAsync();
    
    // CHT008: ValueTask not consumed
    GetDataAsync();
    
    DoSomethingElse();
}

Fixed Code

public async Task ProcessDataAsync()
{
    // Await the operations
    await _asyncEvent.WaitAsync();
    var data = await GetDataAsync();
    
    DoSomethingElse();
}

// Or if fire-and-forget is intentional
public void ProcessData()
{
    // Explicit discard shows intent
    _ = _asyncEvent.WaitAsync();
    _ = GetDataAsync();
    
    DoSomethingElse();
}

Impact on Pooled Primitives

When using CryptoHives.Foundation.Threading async primitives:

var asyncEvent = new AsyncAutoResetEvent();

// BAD: Pooled IValueTaskSource is never returned to pool
asyncEvent.WaitAsync(); // CHT008

// GOOD: IValueTaskSource is returned after await
await asyncEvent.WaitAsync();

Each unconsumed ValueTask from these primitives:

  • Leaks a pooled IValueTaskSource instance
  • May eventually exhaust the pool under high load
  • Causes increased memory allocation as new instances are created

Fire-and-Forget Pattern

If you need fire-and-forget behavior, handle it explicitly:

public static class FireAndForget
{
    public static async void SafeFireAndForget(
        this ValueTask valueTask,
        Action<Exception>? onException = null)
    {
        try
        {
            await valueTask.ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            onException?.Invoke(ex);
        }
    }
}

// Usage
GetValueTask().SafeFireAndForget(ex => _logger.LogError(ex, "Operation failed"));

See Also