S8949 — The overload accepting a 'CancellationToken' should be used

Language
C#
Type
Bug
Severity
Major
Tags
async, concurrency, performance

Why is this an issue?

When a CancellationToken is available but not forwarded to a method that has an overload accepting one, the cancellation request is silently ignored.

The caller’s intent to cancel long-running operations is not propagated, which means cooperative cancellation is broken:

The rule detects any accessible token source:

Exceptions

The rule does not flag calls where CancellationToken.None is explicitly passed as an argument. Pass CancellationToken.None to suppress the rule when cancellation must not apply, such as during disposal or post-cancellation cleanup.

This follows the "point of no cancellation" pattern: once a method has started work it cannot safely revert, cancellation should no longer be honored, and any further token should be forwarded as CancellationToken.None instead of the original token:


public async ValueTask DisposeAsync()
{
    await _stream.FlushAsync(CancellationToken.None); // Compliant: disposal must complete regardless of cancellation
}

How to fix it

Forward the accessible CancellationToken to the overload that accepts it.

Code examples

Noncompliant code example


public async Task SaveAsync(string path, string content, CancellationToken token = default)
{
    await File.WriteAllTextAsync(path, content, Encoding.UTF8); // Noncompliant - pass 'token' to allow cancellation
}

Compliant solution


public async Task SaveAsync(string path, string content, CancellationToken token = default)
{
    await File.WriteAllTextAsync(path, content, Encoding.UTF8, token);
}

Noncompliant code example


[TestClass]
public class MyTests
{
    public TestContext TestContext { get; set; }

    [TestMethod]
    public async Task MyTest()
    {
        await httpClient.GetStringAsync(url); // Noncompliant
    }
}

Compliant solution


[TestClass]
public class MyTests
{
    public TestContext TestContext { get; set; }

    [TestMethod]
    public async Task MyTest()
    {
        await httpClient.GetStringAsync(url, TestContext.CancellationToken);
    }
}

When the noncompliant call is inside a static local function or static lambda, the token cannot be captured from the enclosing scope. Instead, extend the parameter list to accept a CancellationToken and pass it from the call site.

Noncompliant code example


public async Task ProcessAsync(CancellationToken token = default)
{
    await ProcessItemAsync("item");

    static async Task ProcessItemAsync(string item)
    {
        await File.WriteAllTextAsync("log.txt", item, Encoding.UTF8); // Noncompliant
    }
}

Compliant solution


public async Task ProcessAsync(CancellationToken token = default)
{
    await ProcessItemAsync("item", token);

    static async Task ProcessItemAsync(string item, CancellationToken token)
    {
        await File.WriteAllTextAsync("log.txt", item, Encoding.UTF8, token);
    }
}

Noncompliant code example


private readonly SemaphoreSlim _semaphore = new(1, 1);

public void Acquire(CancellationToken? ct = null)
{
    if (ct.HasValue)
        _semaphore.Wait(ct.Value);
    else
        _semaphore.Wait(); // Noncompliant - pass 'ct.GetValueOrDefault()' to allow cancellation
}

Compliant solution


private readonly SemaphoreSlim _semaphore = new(1, 1);

public void Acquire(CancellationToken? ct = null)
{
    _semaphore.Wait(ct.GetValueOrDefault());
}

Resources

Documentation

↑ Back to top