Asynchronous programming is essential for modern applications that communicate with databases, call APIs, read files, or perform other I/O operations. F# already provides asynchronous workflows and task computation expressions, but .NET 11 RC 2 introduces a new option for library authors who want to build asynchronous APIs on the .NET runtime's native async execution model.

The new runtime async support allows F# developers to create custom computation-expression builders that use runtime-managed suspension and continuation instead of relying on the traditional compiler-generated async state machine.

This is an important distinction. The feature does not replace the existing async {} or task {} expressions in FSharp.Core. Instead, it provides compiler-recognized building blocks that library authors can use to experiment with alternative asynchronous programming abstractions.

In this article, we will examine how runtime async works, how to understand the new F# compiler intrinsics, and what developers should consider before using the feature in a real application.

What Is Runtime Async in .NET 11?

In traditional asynchronous execution, the compiler transforms an asynchronous method into a representation that can preserve its state when an operation suspends. When the awaited operation completes, execution resumes from the appropriate point.

With runtime async, the .NET runtime manages suspension and resumption for methods marked for this execution model.

The F# compiler provides special intrinsics that allow supported methods to participate in this mechanism. Instead of relying on the conventional compiler-generated async state machine for those methods, the runtime handles the suspension and continuation behavior.

The intended benefits include improved async debugging, clearer live call stacks, and opportunities to reduce overhead in asynchronous execution.

However, these are goals of the broader runtime async implementation, not a guarantee that every F# application will become faster. Actual performance depends on the workload, compiler output, runtime, and API design.

What's New in F# RC 2?

.NET 11 RC 2 introduces preview support for building custom F# computation-expression builders on top of runtime async.

The compiler provides two important operations through Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers and System.Runtime.CompilerServices.AsyncHelpers.

API

Purpose

__runtimeAsyncReturn

Produces a Task<T> result through the runtime async mechanism

__runtimeAsyncReturnUnit

Produces a non-generic Task result

__runtimeAsyncReturnValueTask

Supports a ValueTask<T> result

__runtimeAsyncReturnValueTaskUnit

Supports a non-generic ValueTask result

AsyncHelpers.Await

Suspends supported runtime-async execution until a task or value task completes

These are compiler-recognized intrinsics rather than ordinary functions intended to be called without the required compiler support.

The feature is marked as a preview. The existing F# task {} and async {} builders remain unchanged, and FSharp.Core does not yet provide a built-in computation-expression builder using this runtime async mechanism. <Cite refs={["turn964729view0"]}/>

Understanding F# Computation Expressions

F# computation expressions provide a structured way to describe workflows involving operations such as asynchronous execution, resource management, and iteration.

For example, a conventional task expression can perform an asynchronous operation and use its result:

open System.Threading.Tasks

let getValue () : Task<int> =
    Task.FromResult 42

let calculate () =
    task {
        let! value = getValue ()
        return value + 8
    }

The let! expression obtains the result of the asynchronous operation, while return supplies the final value.

This pattern is already supported by F#. Runtime async does not make this existing code automatically use a different execution model.

Instead, the new support gives library authors the primitives needed to build their own compatible computation-expression abstractions.

How Runtime Async Works in F#

The following example illustrates the core idea behind the new compiler intrinsics.

open System.Threading.Tasks
open Microsoft.FSharp.Core.CompilerServices

let getValue () : Task<int> =
    Task.FromResult 42

let calculate () : Task<int> =
    StateMachineHelpers.__runtimeAsyncReturn (
        let value =
            System.Runtime.CompilerServices.AsyncHelpers.Await(
                getValue ()
            )

        value + 8
    )

This example demonstrates the relationship between the return intrinsic and the await operation. The return intrinsic establishes the runtime-async method boundary, while AsyncHelpers.Await handles the asynchronous suspension point.

The example is intended to illustrate the API shape, not to replace the existing task {} builder in an application. The feature requires the appropriate preview language version, target reference assemblies, and runtime support. It should be compiled and validated against the matching .NET 11 SDK and runtime before being used in a real project.

Why the Return Intrinsic Matters

The return intrinsic tells the compiler that the method should use the runtime async execution model.

Without that recognized boundary, an emitted suspension operation cannot safely be treated as a runtime-async suspension point.

This is why the new APIs should not be viewed as simple replacements for return or let! in arbitrary F# code. They participate in a compiler and runtime contract.

Why Awaiting Must Be Explicit

Runtime async does not automatically flatten nested asynchronous results in the same way a developer might expect from a conventional computation expression.

When the code needs the result of a task before continuing, the appropriate await intrinsic must be used.

This explicit behavior matters when designing custom builders because the builder must determine how supported asynchronous inputs are awaited and how their results are passed to the next computation.

Building a Custom Computation Expression

The main audience for this feature is library authors who want to create their own task-like computation expressions.

A custom builder can define how a computation is delayed, how intermediate results are passed between operations, and how the final result is returned.

The following is a simplified conceptual outline of the responsibilities:

type CustomAsyncBuilder() =
    member _.Delay(generator: unit -> 'T) =
        generator

    member inline _.Run(generator: unit -> 'T) =
        StateMachineHelpers.__runtimeAsyncReturn(
            generator ()
        )

This is only an illustrative outline, not a complete production-ready builder. A real implementation must provide the required operations for its supported computation-expression syntax and ensure that suspension operations are emitted in valid runtime-async contexts.

For example, supporting let! requires an appropriate binding operation that awaits the incoming task and passes its result to the continuation.

The F# compiler's prototype builder demonstrates familiar constructs such as let!, return, loops, exception handling, and resource management. That prototype is useful for understanding the mechanism, but it should not be mistaken for a built-in FSharp.Core API. <Cite refs={["turn964729view0"]}/>

How to Enable Preview Support

To evaluate runtime async, use the .NET 11 SDK and target net11.0.

The project must enable the preview F# language version because runtime async is still a preview feature.

<PropertyGroup>
  <TargetFramework>net11.0</TargetFramework>
  <LangVersion>preview</LangVersion>
</PropertyGroup>

The TargetFramework property selects the .NET 11 target, while LangVersion enables the preview language features required by the compiler.

The target reference assemblies and executing runtime must both support runtime async. Compiling against newer reference assemblies and running the application on an older runtime is not a supported configuration.

After configuring the project, build it with the matching .NET 11 SDK and run it using the corresponding runtime. Test the exact computation-expression implementation you intend to use rather than assuming that every custom builder shape is supported.

Runtime Async Compared With Existing F# Async APIs

Feature

async {}

task {}

Runtime async support

Primary purpose

F# asynchronous workflows

Task-based asynchronous programming

Custom builders using runtime-managed async execution

Existing support

Established F# API

Established F# API

Preview support

Result model

Async<T>

Task<T> or related task types

Supported Task and ValueTask return types

Custom builder required

Not for ordinary use

Not for ordinary use

Yes, for the new runtime-async approach

Recommended for existing applications

Yes, when appropriate

Yes, when appropriate

Evaluate carefully for specific requirements

The key difference is not simply syntax. Runtime async changes how supported asynchronous methods are represented and resumed by the runtime.

For ordinary application development, the existing builders remain the straightforward choice unless there is a specific reason to experiment with runtime async.

Advantages and Disadvantages

Advantages

Runtime-managed execution: The runtime handles suspension and continuation for supported methods.

Custom abstraction support: Library authors can build new computation expressions around the runtime async model.

Potential debugging improvements: Runtime async is designed to provide more useful live call stacks and debugging behavior.

Task and ValueTask support: The intrinsics cover the supported generic and non-generic task result shapes.

Disadvantages

Preview status: The API surface and implementation are still evolving.

Additional implementation complexity: A custom builder must correctly implement its supported operations and respect compiler restrictions.

Limited built-in support: FSharp.Core does not yet ship a builder based on these mechanics.

Compatibility requirements: The feature requires matching compiler, target framework, reference assemblies, and runtime support.

Common Mistakes and Troubleshooting

The Compiler Rejects Runtime Async Syntax

Confirm that the project uses LangVersion set to preview and targets a framework whose reference assemblies expose the required runtime async support.

Existing task {} Code Behaves as Before

This is expected. The existing task and async builders have not been switched to runtime async automatically.

The Runtime Does Not Support the Compiled Code

Use the matching .NET 11 runtime. Successful compilation against newer reference assemblies does not establish compatibility with an older runtime.

A Custom Builder Fails at a Suspension Point

Review how the builder handles task results and where it places the await intrinsic. Runtime async imposes constraints on generated suspension points, so not every arbitrary computation-expression implementation will be valid.

Summary

.NET 11 RC 2 adds preview runtime async support for F# library authors. The compiler-recognized return and await intrinsics allow custom computation-expression builders to use runtime-managed suspension and continuation instead of relying on conventional compiler-generated async state machines.

The feature opens the door to new asynchronous abstractions, but it is not a replacement for the existing F# async {} and task {} builders. Those APIs remain unchanged.

For most application developers, the best approach is to continue using the established builders. Library authors interested in custom async abstractions can evaluate runtime async with the matching .NET 11 SDK and runtime, while testing compiler restrictions, exception handling, resource cleanup, and real asynchronous workloads before adopting it more broadly.