← back to index

S6966 — Awaitable method should be used

Language: C#  |  Type: CODE_SMELL  |  Severity: Major

Tags: async-await

In an async method, any blocking operations should be avoided.

Why is this an issue?

Using a synchronous method instead of its asynchronous counterpart in an async method blocks the execution and is considered bad practice for several reasons:

Resource Utilization

Each thread consumes system resources, such as memory. When a thread is blocked, it’s not doing any useful work, but it’s still consuming these resources. This can lead to inefficient use of system resources.

Scalability

Blocking threads can limit the scalability of your application. In a high-load scenario where many operations are happening concurrently, each blocked thread represents a missed opportunity to do useful work. This can prevent your application from effectively handling increased load.

Performance

Blocking threads can degrade the performance of your application. If all threads in the thread pool become blocked, new tasks can’t start executing until an existing task completes and frees up a thread. This can lead to delays and poor responsiveness.

Instead of blocking, it’s recommended to use the async operator with async methods. This allows the system to release the current thread back to the thread pool until the awaited task is complete, improving scalability and responsiveness.

How to fix it

Code examples

Noncompliant code example

public async Task Examples(Stream stream, DbSet<Person> dbSet)
{
    stream.Read(array, 0, 1024);            // Noncompliant
    File.ReadAllLines("path");              // Noncompliant
    dbSet.ToList();                         // Noncompliant in Entity Framework Core queries
    dbSet.FirstOrDefault(x => x.Age >= 18); // Noncompliant in Entity Framework Core queries
}

Compliant solution

public async Task Examples(Stream stream, DbSet<Person> dbSet)
{
    await stream.ReadAsync(array, 0, 1024);
    await File.ReadAllLinesAsync("path");
    await dbSet.ToListAsync();
    await dbSet.FirstOrDefaultAsync(x => x.Age >= 18);
}

Exceptions

The following DbDataReader methods (and their overrides in subtypes) are excluded from this rule:

IsDBNull, GetFieldValue<T>, GetValue, GetBytes, GetChars, GetStream, GetTextReader

When CommandBehavior.SequentialAccess is not specified (the default), ReadAsync buffers the entire row into memory before returning. All subsequent column reads are pure in-memory operations — no I/O occurs — so using their async counterparts adds Task allocation overhead without any benefit.

Microsoft explicitly recommends this pattern:

Always call NextResultAsync, ReadAsync, and either call GetFieldValue for default command behavior or GetFieldValueAsync for sequential access.

— Microsoft Learn — DbDataReader
while (await reader.ReadAsync())            // Compliant: ReadAsync is still preferred
{
    if (!reader.IsDBNull(ordinal))          // Compliant exception: synchronous column read is preferred in default mode
        return reader.GetFieldValue<string>(ordinal); // Compliant exception
}

Note: Read is not exempt — ReadAsync should still be used when iterating rows.

Resources

Documentation

Articles & blog posts