Introducing F# 10

Wait 5 sec.

F# 10 is now shipping with .NET 10 and Visual Studio 2026.This version is a refinement release focused on clarity, consistency, and performance, bringing small but meaningful improvements that make your everyday code more legible and robust.You can get F# 10 today in a few ways:Install the latest .NET 10 SDKInstall the latest Visual Studio 2026 previewWith F# 10, we're continuing our journey to make the language simpler and more performant.Key ergonomic improvements include scoped warning suppression,more consistent syntax for computation expressions,and better support for auto property accessors.The release also includes an infrastructure upgrade to speed up compilation and interactive tooling in the form of the new type subsumption cache.Language Improvements1. Scoped warning suppressionThe first feature I want to introduce for F# 10 is a much-requested one: the ability to suppress warnings in arbitrary code sections.Our compiler now supports the #warnon directive, which pairs with #nowarn to enable or disable warnings within a specific code span.Let's take a look at a motivating example:// We know f is never called with a None.let f (Some a) = // creates warning 25, which we want to suppress // 2000 loc, where the incomplete match warning is beneficialOf course, we can add a #nowarn 25 directive right above the function definition,but without a matching #warnon 25, this will disable the FS0025 warning in the remainder of the file,risking the suppression of legitimate issues elsewhere.With F# 10, you can delimit the section explicitly, applying the warning suppression to as narrow a scope as a single line:#nowarn 25let f (Some x) = // FS0025 suppressed#warnon 25 // FS0025 enabled againConversely, if a warning is disabled globally (e.g., via a compiler flag), you can enable it locally with #warnon.This directive will then apply until a matching #nowarn or the end of the file.Compatibility:This feature is accompanied by several changes that improve the consistency of #nowarn/#warnon directives, leading to more predictable behavior.However, these are breaking changes that might affect your codebase when you update to F# 10.A detailed list of potential issues can be found in the RFC-1146, but here are a couple of examples most likely to occur:Multiline and empty warn directives are no longer allowed. Whitespace between # and nowarn is no longer allowed. Triple-quoted, interpolated, or verbatim strings cannot be used for warning numbers.Script behavior is also affected.In previous versions, a #nowarn directive anywhere in a script applied to the whole compilation.Now, its behavior in scripts matches that in .fs files, applying only until the end of the file or a corresponding #warnon.2. Access modifiers on auto property accessorsA frequent pattern in object-oriented programming is to create publicly readable but privately mutable state.Before F# 10, achieving this required explicit property syntax with backing fields, which added significant boilerplate:type Ledger() = [] val mutable private _Balance: decimal member this.Balance with public get() = this._Balance and private set v = this._Balance instead of the reference-based option type.This avoids a heap allocation for the option wrapper, which is beneficial in performance-critical code.Previous versions of F# always used the heap-allocated option type for optional parameters, even when the parameter was absent.For high-throughput scenarios or inner-loop object creation, this imposed unnecessary GC pressure.Developers working on performance-sensitive code had no way to avoid these allocations:// Prior to F# 10: always uses reference optiontype X() = static member M(?x : string) = match x with | Some v -> printfn "Some %s" v | None -> printfn "None"Now you can use the [] attribute on an optional parameter to leverage the struct-backed ValueOption and eliminate allocations when the argument is absent:type X() = static member M([] ?x : string) = match x with | ValueSome v -> printfn "ValueSome %s" v | ValueNone -> printfn "ValueNone"Choose this struct-based option for small values or frequently constructed types where allocation pressure matters.Use the default reference-based option when you rely on existing pattern matching helpers, need reference semantics, or when the performance difference is negligible.This feature strengthens parity with other F# language constructs that already support ValueOption.4. Tail-call support in computation expressionsComputation-expression builders (for example, coroutines or other builders implemented with resumable state machines) can now opt into tail-call optimizations.During desugaring, the compiler recognizes when an expression like return!, yield! or do! appears in a tail position and,when the builder provides special methods, routes those calls to the optimized entry points.If a builder implements:ReturnFromFinal, the compiler will call it for a tail return! (falling back to ReturnFrom if the final variant is absent).YieldFromFinal, the compiler will call it for a tail yield! (falling back to YieldFrom if absent).For a terminal do!, the compiler will prefer ReturnFromFinal, then YieldFromFinal, before falling back to the normal Bind pathway.These *Final members are optional and exist purely to enable optimization: they allow builders to short-circuit continuations or otherwise relinquish resources early.Builders that do not provide these members keep their existing semantics unchanged.Examples:coroutine { yield! subRoutine() // tail position -> YieldFromFinal if available}coroutine { try yield! subRoutine() // not tail -> normal YieldFrom finally ()}Compatibility:This change can be breaking if a builder already defines members with these names.In most cases, it is backward-compatible: existing builders continue to work without modification when compiled with F# 10.Older compilers will simply ignore the new *Final methods, so builders that must remain compatible with earlier compiler versions should not assume those methods will be invoked.5. Typed bindings in computation expressions without parenthesesA long-standing inconsistency in type annotation syntax for computation expression bindings has been resolved.You can now add type annotations on let!, use!, and and! bindings without wrapping the identifier in parentheses.Prior versions of F# required parentheses for type annotations in computation expression bindings.For example, let! (x: int) = fetchA() was valid, but the more natural let! x: int = fetchA() would cause an error.This forced developers to use visually noisy parentheses even for simple type annotations:async { let! (a: int) = fetchA() and! (b: int) = fetchB() use! (d: MyDisposable) = acquireAsync() return a + b}Now you can write type annotations without parentheses, matching the style of ordinary let bindings:async { let! a: int = fetchA() and! b: int = fetchB() use! d: MyDisposable = acquireAsync() return a + b}6. Allow _ in use! bindingsPrior versions of F# rejected the discard pattern in use! bindings, even when the resource value itself was never referenced.This inconsistency with regular use bindings forced developers to create throwaway identifiers like __ or _ignored just to satisfy the compiler.The discard pattern (_) now works in use! bindings within computation expressions.F# 10 allows you to use _ directly when binding asynchronous resources whose valuesare only needed for lifetime management, without being forced to provide a named identifier.Now you can use the discard pattern directly, clarifying your intent and matching the behavior of use:counterDisposable { use! _ = new Disposable() // logic}7. Rejecting pseudo-nested modules in typesStructural validation has been tightened in this release to reject misleading module placement within types.F# 10 now raises an error when a module declaration appears indented at the same structural levelinside a type definition, preventing a common source of confusion about module scoping.Previous versions of F# accepted module declarations indented within type definitions,but these modules were actually created as siblings to the type rather than being nested within it.This indentation pattern frequently misled developers into believing they had created a nested module, resulting in unexpected scoping behavior:type U = | A | B module M = // Silently created a sibling module, not nested let f () = ()Now, this pattern raises error FS0058, forcing you to clarify your intent with proper module placement:type U = | A | Bmodule M = let f () = ()8. Deprecation warning for omitted seqA deprecation warning now appears for bare sequence expressions that omit the seq builder.F# 10 warns when you use bare range braces like { 1..10 } or similar forms, encouraging you to use theexplicit seq { ... } form for consistency with the broader computation expression model.Historically, F# allowed a special-case “sequence comprehension lite” syntax where the seq keyword could be omitted.This diverged from how other computation expressions work and created an inconsistency in the language:{ 1..10 } |> List.ofSeq // implicit sequenceNow, the compiler warns about this pattern and encourages the explicit form that clarifies semantics:seq { 1..10 } |> List.ofSeqThis is currently a warning, not an error, giving you time to update your codebase.The explicit seq form improves code clarity and consistency with other computation expressions.Future versions of F# may make this an error, so we recommend adopting the explicit syntax when updating code.9. Attribute target enforcementAttribute target validation will be enforced with this release across all language constructs.F# 10 validates that attributes are only applied to their intended targets by checking AttributeTargetsacross let-bound values, functions, union cases, implicit constructors, structs, and classes.Previous versions of F# silently allowed attributes to be misapplied to incompatible targets.This caused subtle bugs, such as test attributes being ignored when you forgot () to make a functionor analyzer directives failing to take effect, leading to confusing CI discrepancies:[]let ``this is not a function`` = // Silently ignored, not a test! Assert.True(false)Now, the compiler enforces attribute targets and raises a warning when attributes are misapplied:[]//^^^^ - warning FS0842: This attribute cannot be applied to property, field, return value. Valid targets are: methodlet ``works correctly`` = Assert.True(true)Compatibility:This is a breaking change that may reveal previously silent issues in your codebase.The early errors prevent test discovery problems and ensure that attributes like analyzers and decorators take effect as intended.FSharp.Core enhancement - support for and! in task expressionsThis release brings a single improvement to the FSharp.Core library: support for and! in the task computation expression.Using task is a popular way to work with asynchronous workflows in F#, especially when interop with C# is required.However, until recently there was no concise way to await multiple tasks concurrently in a computation expression.Perhaps you started with code that awaited computations sequentially:// Awaiting sequentiallytask { let! a = fetchA() let! b = fetchB() return combineAB a b}If you then wanted to change it to await them concurrently, you would typically use Task.WhenAll:// Use explicit Task combinator to await concurrentlytask { let ta = fetchA() let tb = fetchB() let! results = Task.WhenAll([| ta; tb |]) return combineAB ta.Result tb.Result}With F# 10, you can instead write a more idiomatic version using and!:task { let! a = fetchA() and! b = fetchB() return combineAB a b}This combines the semantics of the second snippet with the simplicity of the first.Performance & ToolingType subsumption cacheThis release also introduces a new type subsumption cache to accelerate type checking and improve IDE responsiveness, especially in projects with complex type hierarchies.In F# 10, the compiler memoizes the results of type relationship checks, reducing redundant computations and improving overall compiler and tooling performance.Previously, the F# compiler would repeatedly perform expensive subsumption checks when dealing with large type hierarchies, such as those involving numerous numeric primitives or many interface implementations.This could lead to noticeable latency during type inference and IDE operations, particularly in large solutions or during long editing sessions, consuming extra CPU and memory.Now, the new type subsumption cache stores the results of these checks.When the compiler needs to determine if one type can be used in place of another, it first consults the cache.This memoization avoids re-computing the same type relationships, leading to faster compilation and more responsive IntelliSense.No code changes are required to benefit from this improvement; the performance gains are automatic.Better trimming by defaultF# 10 removes a long‑standing bit of friction with trimming F# assemblies:you no longer have to hand‑maintain an ILLink.Substitutions.xml file just to strip large F# metadata resource blobs (signature/optimization data) that aren't needed at runtime in the final application.When you publish with trimming enabled (PublishTrimmed=true), the F# build now auto‑generates a substitutions file that targets the tooling‑only F# resources.The result: smaller output by default, less boilerplate, and one fewer maintenance hazard.If you need full manual control you can still add your own substitutions file.The auto-generation can be turned off with a property, like so: false.Parallel compilation in previewAn exciting update for F# users looking to reduce their projects' compilation times is the progressing stabilization of the parallel compilation features.Starting with .NET 10, three features: graph-based type checking, parallel IL code generation, and parallel optimization are grouped together under the ParallelCompilation project property.Currently, this setting is turned on by default for projects that are using LangVersion=Preview, and we plan to turn it on for everyone in .NET 11.Be sure to give it a try and see if it speeds up your compilation.However, if you want to opt out but still enjoy other preview features, then set the ParallelCompilation to false.Typecheck-only mode for scriptsF# is a great scripting language, and we want to ensure that the tooling also supports that use case.That is why we're extending the --typecheck-only compilation flag to also work for .fsx scripts, where it's arguably the most beneficial.In F# 9 and earlier, there was no easy way to validate syntax and type correctness of a script without running the code, which would often produce side effects.With this release, you can simply use the --typecheck-only flag while invoking fsi to typecheck the script without execution.This feature makes it easy to add CI gates that will be able to catch script rot in your codebase.Known issue caveat:As of publishing this post, there is a known bug that severely limits the benefits of using --typecheck-only with scripts that include others via #load.In that case type checking will conclude prematurely after processing the first loaded source file. However, the fix has already been implemented and will be included in the upcoming patch release 10.0.200.Thanks and acknowledgementsF# is developed as a collaboration between the .NET Foundation, the F# Software Foundation, their members and other contributors including Microsoft.The F# community is involved at all stages of innovation, design, implementation and delivery and we’re proud to be a contributing part of this community.In the past year, we merged commits from 25 contributors.We would like to thank all of you, as well as many others who filed issues, raised and voted on language suggestions, and contributed to language design.In particular, we explicitly call out some of the most active community contributors:@majocha for a multitude of contributions, including infrastructure, testing and tooling improvements.@Martin521 most notably for a herculean effort on the scoped #nowarn feature design and implementation.@edgarfgp for his continued efforts in the area of error handling and diagnostics, as well as great improvements to the parser.@auduchinok for his work on improving tooling via fixes to parser recovery and other areas, as well as contributions to testing infrastructure.Contributor showcaseAs is tradition in our release announcements, the contributor showcase section is where we celebrate some of our most impactful open-source community contributors.While our long-time contributor Jakub Majocha received a well-deserved shout-out in a .NET 8 announcement two years ago, his outstanding work over the past year on the F# compiler has earned him one here in the F# release blog post.This time, he is going to share a little about his approach to contributing to the repo:Looking back at my PR history I see that most of them are fixes to things that I broke 🙂That's why as a hobbyist I try to stick to contributions that have low risk of breaking something serious. For example I had a lot of fun making the tests run in parallel.I'm also always looking for low-hanging fruit, there still are quite a few. For example you can revive some unfinished PRs that were already almost done:Type subsumption cache was one and the other was tail-call support in computation expressions - my first ever RFC implemented.I have an interest in tooling and its performance, so another surprising win that I'm very happy with is reducing StackGuard related slowdowns.I had a hunch about StackGuard impact on editor performance, and experimented for some time with various approaches.Adding some instrumentation to collect metrics was the most helpful here and the solution turned out to be very straightforward.I'd like to add huge thanks to the Amplifying F# collective for encouraging my attempts and sponsoring my Copilot subscription.If you read it carefully, you might have noticed a teaser of an upcoming performance improvement that didn't make it to this release, but we are very excited to bring to F# 11.And just like Jakub, we would like to recognize Amplifying F# for their invaluable support of community contributors.Efforts like theirs help make the F# ecosystem thrive.What's next?Work on F# 11 is already under way: more performance wins, languageimprovements, and tooling upgrades.Go to our GitHub repo to jump into discussions, help wanted issues or good first issues for new contributors.