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 operation continues running even after cancellation is requested, wasting resources.
- Timeouts and deadlines set by callers are not respected.
- The application becomes less responsive and harder to shut down gracefully.
The rule detects any accessible token source:
- a
CancellationTokenparameter at any position - a
CancellationTokenfield or property - a
CancellationToken?parameter, field, or property; forwarded as.GetValueOrDefault() - an
AsyncLocal<CancellationToken>orThreadLocal<CancellationToken>parameter, field, or property; forwarded as.Value - a
CancellationTokenobtained via a property of an accessible object, e.g._cts.Token,HttpContext.RequestAborted
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
- Microsoft Learn - CA2016: Forward the CancellationToken parameter to methods that take one
- Microsoft Learn - Cancellation in managed threads
- Microsoft Learn - Recommended patterns for CancellationToken