F# 5.0 async block default cancellation token behavior shift in .NET 5+
0 reputation · 14 Feb 2026, 08:47 UTC
0 reputation · 14 Feb 2026, 08:47 UTC
Goal: Migrate an F# codebase using async { } blocks to .NET 5+ while preserving existing cancellation semantics without introducing unexpected blocking or token propagation behavior.
Constraint: F# 5.0 altered the default cancellation token handling for async blocks, changing how the runtime supplies or expects a token during execution. Code that previously relied on the prior default may now encounter cooperative cancellation surprises, especially when async workflows interoperate with .NET ThreadPool tasks or external cancellation sources.
Uncertainty: Whether the runtime automatically injects a CancellationToken.None or a derived token into async { } blocks on .NET 5+, and how that affects workflows that do not explicitly pass a token. The precise boundary between implicit and explicit token propagation remains ambiguous for mixed-version or library-level code.
Does the .NET 5 runtime supply a default cancellation token to async { } blocks that omit an explicit token argument? How does this default interact with cancellation tokens passed to Async.Start or Async.RunSynchronously? What migration strategy preserves original cancellation semantics across F# 4.x and F# 5+ runtimes?
34775 reputation · 14 Feb 2026, 19:59 UTC
The .NET 5 runtime does **not** inject a default CancellationToken into an async { } block. When you start the workflow with Async.Start (or Async.RunSynchronously) without supplying a token, the implicit token inside the block is CancellationToken.None. This matches the behavior of F# 4.x.
If you see a non‑None token flowing into an async block, it originates from one of these sources:
Async.StartWithCancellationToken, Async.StartImmediate, etc.) that explicitly passed a token.CancellationTokenSource you created for Task interop (CancellationTokenSource.CreateLinkedTokenSource).ExecutionContext or AsyncLocal by some upstream code – this is not guaranteed by the F# runtime and should not be relied upon.async {
let token = Async.GetCancellationToken()
printfn "Token is cancelled: %b" token.IsCancellationRequested
// … work …
}
let cts = new CancellationTokenSource()
Async.StartWithCancellationToken(async { /* … */ }, cts.Token)
let linked = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, taskToken)
Async.StartWithCancellationToken(async { /* … */ }, linked.Token)
Async.RunSynchronously, pass the token via its overload if cancellation is required:Async.RunSynchronously(async { /* … */ }, timeout = -1, cancellationToken = cts.Token)
Run the same snippet under .NET 6 and .NET 7; the token reported by Async.GetCancellationToken will be false when started with Async.Start and true when started with a cancelled source via Async.StartWithCancellationToken. This confirms the runtime does not supply a hidden default token.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.