How to use the cancellable task family for request cancellation
Use the cancellable builders when cancellation is part of the operation's contract.
In web applications, the boundary token is commonly HttpContext.RequestAborted; in other hosts, it may come from a message processor, worker shutdown token, or command timeout.
The cancellable task family uses the same shape:
CancellableTask<'T>isCancellationToken -> Task<'T>CancellableValueTask<'T>isCancellationToken -> ValueTask<'T>cancellablePoolingValueTaskalso returnsCancellationToken -> ValueTask<'T>and uses a pooling method builder on .NET 6+
Pass the request token at the boundary
The important boundary rule is simple: keep the cancellation-aware workflow as a value, then start it by passing the request token.
This sample uses a minimal request context so the example stays focused on the token flow. In ASP.NET, this token is HttpContext.RequestAborted.
type RequestContext = { RequestAborted: CancellationToken }
type OrderId = OrderId of int
type Order = {
Id: OrderId
Total: decimal
CanCompleteSynchronously: bool
}
module OrderStore =
let loadAsTask (orderId: OrderId) (cancellationToken: CancellationToken) =
task {
do! Task.Delay(1, cancellationToken)
return {
Id = orderId
Total = 42.00M
CanCompleteSynchronously = false
}
}
let loadAsValueTask (orderId: OrderId) (cancellationToken: CancellationToken) =
cancellationToken.ThrowIfCancellationRequested()
let (OrderId id) = orderId
if id = 0 then
ValueTask<Order> {
Id = orderId
Total = 0.00M
CanCompleteSynchronously = true
}
else
task {
do! Task.Delay(1, cancellationToken)
return {
Id = orderId
Total = 42.00M
CanCompleteSynchronously = false
}
}
|> ValueTask<Order>
let writeAudit (order: Order) (cancellationToken: CancellationToken) =
task {
do! Task.Delay(1, cancellationToken)
return order
}
Use cancellableTask when the work is actually async
Use cancellableTask when the operation naturally returns Task<'T> or is expected to do asynchronous work.
Inside the builder, CancellableTask.getCancellationToken() gives you the token supplied by the caller.
let loadOrderWithTask (orderId: OrderId) : CancellableTask<Order> =
cancellableTask {
let! cancellationToken = CancellableTask.getCancellationToken ()
let! order = OrderStore.loadAsTask orderId cancellationToken
return! OrderStore.writeAudit order cancellationToken
}
let handleTaskRequest (ctx: RequestContext) (orderId: OrderId) =
task { return! loadOrderWithTask orderId ctx.RequestAborted }
Use cancellableValueTask when the work might complete synchronously
Use cancellableValueTask when the same cancellation model should return ValueTask<'T>.
This is useful for APIs that often complete synchronously, such as cache hits, but sometimes need asynchronous work.
let loadOrderWithValueTask (orderId: OrderId) : CancellableValueTask<Order> =
cancellableValueTask {
let! cancellationToken = CancellableValueTask.getCancellationToken ()
let! order = OrderStore.loadAsValueTask orderId cancellationToken
return order
}
let handleValueTaskRequest (ctx: RequestContext) (orderId: OrderId) =
task { return! loadOrderWithValueTask orderId ctx.RequestAborted }
Consider cancellablePoolingValueTask for allocation-sensitive .NET 6+ code
Use cancellablePoolingValueTask when you want the CancellableValueTask<'T> shape in allocation-sensitive .NET 6+ code.
It uses PoolingAsyncValueTaskMethodBuilder, so it is an advanced option for hot paths where ValueTask already makes sense.
let loadOrderWithPoolingValueTask (orderId: OrderId) : CancellableValueTask<Order> =
cancellablePoolingValueTask {
let! order = fun ct -> OrderStore.loadAsValueTask orderId ct
return order
}
let handlePoolingValueTaskRequest (ctx: RequestContext) (orderId: OrderId) =
task { return! loadOrderWithPoolingValueTask orderId ctx.RequestAborted }
Await from Async when needed
The Async.AwaitCancellableTask and Async.AwaitCancellableValueTask helpers preserve Async's cancellation token.
Use them at interop boundaries where the surrounding workflow is still Async<'T>.
let loadWithAsyncInterop orderId =
async {
let! order = loadOrderWithValueTask orderId
return order.Total
}
Run the examples
At the application boundary, pass the request token once. Every nested bind can then retrieve or receive the same token.
let request = {
RequestAborted = CancellationToken.None
}
let loadedWithTask =
handleTaskRequest request (OrderId 42)
|> fun task -> task.GetAwaiter().GetResult()
let loadedWithValueTask =
handleValueTaskRequest request (OrderId 0)
|> fun task -> task.GetAwaiter().GetResult()
let loadedWithPoolingValueTask =
handlePoolingValueTaskRequest request (OrderId 42)
|> fun task -> task.GetAwaiter().GetResult()
Choose the shape
Builder |
Return shape |
Use when |
|---|---|---|
|
|
The work is actually asynchronous or the called APIs already return |
|
|
The operation may complete synchronously, and you want to optimize that path. |
|
|
The code targets .NET 6+ and is allocation-sensitive enough that pooling is worth considering. |
All three are cold and multi-start: creating the value does not start the work, and each call with a token starts a fresh operation.
[<Struct>] type CancellationToken = new: canceled: bool -> unit member Equals: other: obj -> bool + 1 overload member GetHashCode: unit -> int member Register: callback: Action -> CancellationTokenRegistration + 4 overloads member ThrowIfCancellationRequested: unit -> unit member UnsafeRegister: callback: Action<obj,CancellationToken> * state: obj -> CancellationTokenRegistration + 1 overload static member (<>) : left: CancellationToken * right: CancellationToken -> bool static member (=) : left: CancellationToken * right: CancellationToken -> bool member CanBeCanceled: bool member IsCancellationRequested: bool ...
<summary>Propagates notification that operations should be canceled.</summary>
--------------------
CancellationToken ()
CancellationToken(canceled: bool) : CancellationToken
union case OrderId.OrderId: int -> OrderId
--------------------
type OrderId = | OrderId of int
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
val decimal: value: 'T -> decimal (requires member op_Explicit)
--------------------
type decimal = Decimal
--------------------
type decimal<'Measure> = decimal
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>
--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> + 1 overload member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 4 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>
--------------------
Task(action: Action) : Task
Task(action: Action, cancellationToken: CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task
--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task.Delay(millisecondsDelay: int) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider) : Task
Task.Delay(delay: TimeSpan, cancellationToken: CancellationToken) : Task
Task.Delay(millisecondsDelay: int, cancellationToken: CancellationToken) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider, cancellationToken: CancellationToken) : Task
[<Struct>] type ValueTask = new: source: IValueTaskSource * token: int16 -> unit + 1 overload member AsTask: unit -> Task member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredValueTaskAwaitable member Equals: obj: obj -> bool + 1 overload member GetAwaiter: unit -> ValueTaskAwaiter member GetHashCode: unit -> int member Preserve: unit -> ValueTask static member (<>) : left: ValueTask * right: ValueTask -> bool static member (=) : left: ValueTask * right: ValueTask -> bool static member FromCanceled: cancellationToken: CancellationToken -> ValueTask + 1 overload ...
<summary>Provides an awaitable result of an asynchronous operation.</summary>
--------------------
[<Struct>] type ValueTask<'TResult> = new: source: IValueTaskSource<'TResult> * token: int16 -> unit + 2 overloads member AsTask: unit -> Task<'TResult> member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredValueTaskAwaitable<'TResult> member Equals: obj: obj -> bool + 1 overload member GetAwaiter: unit -> ValueTaskAwaiter<'TResult> member GetHashCode: unit -> int member Preserve: unit -> ValueTask<'TResult> member ToString: unit -> string static member (<>) : left: ValueTask<'TResult> * right: ValueTask<'TResult> -> bool static member (=) : left: ValueTask<'TResult> * right: ValueTask<'TResult> -> bool ...
<summary>Provides a value type that wraps a <see cref="T:System.Threading.Tasks.Task`1" /> and a <typeparamref name="TResult" />, only one of which is used.</summary>
<typeparam name="TResult">The result.</typeparam>
--------------------
ValueTask ()
ValueTask(task: Task) : ValueTask
ValueTask(source: Sources.IValueTaskSource, token: int16) : ValueTask
--------------------
ValueTask ()
ValueTask(task: Task<'TResult>) : ValueTask<'TResult>
ValueTask(result: 'TResult) : ValueTask<'TResult>
ValueTask(source: Sources.IValueTaskSource<'TResult>, token: int16) : ValueTask<'TResult>
module CancellableTask from IcedTasks.CancellableTasks.CancellableTasks
<summary> Contains functional helper functions for composing and converting CancellableTask values. </summary>
--------------------
type CancellableTask = CancellationToken -> Task
<summary> CancellationToken -> Task </summary>
--------------------
type CancellableTask<'T> = CancellationToken -> Task<'T>
<summary> CancellationToken -> Task<'T> </summary>
<summary> Builds a cancellableTask using computation expression syntax. </summary>
<summary>Gets the default cancellation token for executing computations.</summary>
<returns>The default CancellationToken.</returns>
<category index="3">Cancellation and Exceptions</category>
<example id="default-cancellation-token-1"><code lang="F#"> use tokenSource = new CancellationTokenSource() let primes = [ 2; 3; 5; 7; 11 ] for i in primes do let computation = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken() do! Task.Delay(i * 1000, cancellationToken) printfn $"{i}" } computation tokenSource.Token |> ignore Thread.Sleep(6000) tokenSource.Cancel() printfn "Tasks Finished" </code> This will print "2" 2 seconds from start, "3" 3 seconds from start, "5" 5 seconds from start, cease computation and then followed by "Tasks Finished". </example>
module CancellableValueTask from IcedTasks.CancellablePoolingValueTasks.CancellablePoolingValueTasks
<summary> Contains functional helper functions for composing and converting pooling-backed CancellableValueTask values. </summary>
--------------------
module CancellableValueTask from IcedTasks.CancellableValueTasks.CancellableValueTasks
<summary> Contains functional helper functions for composing and converting CancellableValueTask values. </summary>
--------------------
type CancellableValueTask = CancellationToken -> ValueTask
<summary> CancellationToken -> ValueTask </summary>
--------------------
type CancellableValueTask<'T> = CancellationToken -> ValueTask<'T>
<summary> CancellationToken -> ValueTask<'T> </summary>
<summary> Builds a cancellableValueTask using computation expression syntax. This utilizes <see cref="T:System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1">System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder</see></summary>
<remarks> Instead of needing an attribute the compiler needs to know about like in <see href="https://github.com/dotnet/runtime/issues/49903">dotnet/runtime/issues/49903</see> this is a specific computation expression. </remarks>
val getCancellationToken: unit -> ct: CancellationToken -> ValueTask<CancellationToken>
<summary>Gets the default cancellation token for executing computations.</summary>
<returns>The default CancellationToken.</returns>
<category index="3">Cancellation and Exceptions</category>
<example id="default-cancellation-token-1"><code lang="F#"> use tokenSource = new CancellationTokenSource() let primes = [ 2; 3; 5; 7; 11 ] for i in primes do let computation = cancellableValueTask { let! cancellationToken = CancellableValueTask.getCancellationToken() do! Task.Delay(i * 1000, cancellationToken) printfn $"{i}" } computation tokenSource.Token |> ignore Thread.Sleep(6000) tokenSource.Cancel() printfn "Tasks Finished" </code> This will print "2" 2 seconds from start, "3" 3 seconds from start, "5" 5 seconds from start, cease computation and then followed by "Tasks Finished". </example>
--------------------
val getCancellationToken: unit -> ct: CancellationToken -> ValueTask<CancellationToken>
<summary>Gets the default cancellation token for executing computations.</summary>
<returns>The default CancellationToken.</returns>
<category index="3">Cancellation and Exceptions</category>
<example id="default-cancellation-token-1"><code lang="F#"> use tokenSource = new CancellationTokenSource() let primes = [ 2; 3; 5; 7; 11 ] for i in primes do let computation = cancellableValueTask { let! cancellationToken = CancellableValueTask.getCancellationToken() do! Task.Delay(i * 1000, cancellationToken) printfn $"{i}" } computation tokenSource.Token |> ignore Thread.Sleep(6000) tokenSource.Cancel() printfn "Tasks Finished" </code> This will print "2" 2 seconds from start, "3" 3 seconds from start, "5" 5 seconds from start, cease computation and then followed by "Tasks Finished". </example>
<summary> Builds a cancellablePoolingValueTask using computation expression syntax. This utilizes <see cref="T:System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1">System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder</see> as described in <see href="https://devblogs.microsoft.com/dotnet/async-valuetask-pooling-in-net-5/">Async ValueTask Pooling in .NET 5</see>. </summary>
<remarks> Instead of needing an attribute the compiler needs to know about like in <see href="https://github.com/dotnet/runtime/issues/49903">dotnet/runtime/issues/49903</see> this is a specific computation expression. </remarks>
<summary>Returns an empty <see cref="T:System.Threading.CancellationToken" /> value.</summary>
<returns>An empty cancellation token.</returns>
IcedTasks