loading experience

C#

async/await in C#: best practices, Task and ValueTask

The most common mistakes with asynchronous code and when ValueTask makes sense.

async/await in C#: best practices, Task and ValueTask

Asynchronous code makes applications responsive and scalable, but a few recurring mistakes can cause hangs, lost exceptions or worse performance than synchronous code.

Each layer awaits the next and passes the cancellation token along.
Each layer awaits the next and passes the cancellation token along.

Rule 1: async all the way

Blocking a Task with .Result or .Wait() wastes threads and, in some contexts, causes deadlocks. If a method calls asynchronous code, it must become asynchronous too.

// Avoid
var customer = repo.FindAsync(id).Result;

// Correct
var customer = await repo.FindAsync(id);

Rule 2: no async void

An async void method cannot be awaited and its exceptions cannot be caught with a normal try/catch. Use it only for event handlers; in every other case return Task.

Rule 3: pass the CancellationToken along

public async Task<Report> GenerateAsync(int year, CancellationToken ct)
{
    var data = await _repo.LoadAsync(year, ct);
    return await _engine.ProcessAsync(data, ct);
}

That way a web request cancelled by the user really stops the work, instead of continuing to consume resources.

When to use ValueTask

ValueTask avoids an allocation when the result is often already available, for example read from a cache:

public ValueTask<Product?> FindAsync(int id)
{
    if (_cache.TryGetValue(id, out var p))
        return ValueTask.FromResult<Product?>(p); // no allocation

    return new ValueTask<Product?>(LoadFromDatabaseAsync(id));
}

But it comes with rules: a ValueTask is awaited only once and must not be stored for reuse. When in doubt, Task remains the safer choice; use ValueTask only where a measurement proves the benefit.

Comments (0)

No comments yet.

Leave a comment