Header menu logo IcedTasks

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:

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

cancellableTask

CancellationToken -> Task<'T>

The work is actually asynchronous or the called APIs already return Task<'T>.

cancellableValueTask

CancellationToken -> ValueTask<'T>

The operation may complete synchronously, and you want to optimize that path.

cancellablePoolingValueTask

CancellationToken -> ValueTask<'T>

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.

namespace System
namespace System.Threading
namespace System.Threading.Tasks
namespace IcedTasks
type RequestContext = { RequestAborted: CancellationToken }
Multiple items
[<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
Multiple items
union case OrderId.OrderId: int -> OrderId

--------------------
type OrderId = | OrderId of int
type OrderId = | OrderId of int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
type Order = { Id: OrderId Total: decimal CanCompleteSynchronously: bool }
Multiple items
val decimal: value: 'T -> decimal (requires member op_Explicit)

--------------------
type decimal = Decimal

--------------------
type decimal<'Measure> = decimal
type bool = Boolean
val loadAsTask: orderId: OrderId -> cancellationToken: CancellationToken -> Task<Order>
val orderId: OrderId
val cancellationToken: CancellationToken
val task: TaskBuilder
Multiple items
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(delay: TimeSpan) : Task
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
val loadAsValueTask: orderId: OrderId -> cancellationToken: CancellationToken -> ValueTask<Order>
CancellationToken.ThrowIfCancellationRequested() : unit
val id: int
Multiple items
[<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>
val writeAudit: order: Order -> cancellationToken: CancellationToken -> Task<Order>
val order: Order
val loadOrderWithTask: orderId: OrderId -> CancellableTask<Order>
Multiple items
module CancellableTask from IcedTasks.CancellableTasks.CancellableTasks
<summary> Contains functional helper functions for composing and converting CancellableTask values. </summary>

--------------------
type CancellableTask = CancellationToken -> Task
<summary> CancellationToken -&gt; Task </summary>

--------------------
type CancellableTask<'T> = CancellationToken -> Task<'T>
<summary> CancellationToken -&gt; Task&lt;'T&gt; </summary>
val cancellableTask: CancellableTaskBuilder
<summary> Builds a cancellableTask using computation expression syntax. </summary>
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 = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken() do! Task.Delay(i * 1000, cancellationToken) printfn $"{i}" } computation tokenSource.Token |&gt; 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 OrderStore from Use-cancellable-task-family-for-request-cancellation
val handleTaskRequest: ctx: RequestContext -> orderId: OrderId -> Task<Order>
val ctx: RequestContext
RequestContext.RequestAborted: CancellationToken
val loadOrderWithValueTask: orderId: OrderId -> CancellableValueTask<Order>
Multiple items
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 -&gt; ValueTask </summary>

--------------------
type CancellableValueTask<'T> = CancellationToken -> ValueTask<'T>
<summary> CancellationToken -&gt; ValueTask&lt;'T&gt; </summary>
val cancellableValueTask: CancellableValueTaskBuilder
<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>
Multiple items
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 |&gt; 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 |&gt; 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 handleValueTaskRequest: ctx: RequestContext -> orderId: OrderId -> Task<Order>
val loadOrderWithPoolingValueTask: orderId: OrderId -> CancellableValueTask<Order>
val cancellablePoolingValueTask: CancellablePoolingValueTaskBuilder
<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>
val ct: CancellationToken
val handlePoolingValueTaskRequest: ctx: RequestContext -> orderId: OrderId -> Task<Order>
val loadWithAsyncInterop: orderId: OrderId -> Async<decimal>
val async: AsyncBuilder
Order.Total: decimal
val request: RequestContext
property CancellationToken.None: CancellationToken with get
<summary>Returns an empty <see cref="T:System.Threading.CancellationToken" /> value.</summary>
<returns>An empty cancellation token.</returns>
val loadedWithTask: Order
val task: Task<Order>
Task.GetAwaiter() : Runtime.CompilerServices.TaskAwaiter<Order>
val loadedWithValueTask: Order
val loadedWithPoolingValueTask: Order

Type something to start searching.