Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

F#  Module 1 – Start here: F# for .NET developers

F# projects, file order and compiler messages

Create F# console, library and test projects with dotnet new, fix errors that file order causes, read FS diagnostic codes and control warnings in F# 10.

  • Beginner
  • 10 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create console, library and test projects with dotnet new and explain the Compile order of an .fsproj
  • Fix a 'not defined' error caused by file order instead of reaching for a recursive module
  • Read F# diagnostics by their codes, such as FS0001, FS0039, FS0025 and FS0020
  • Configure warnings for a few lines with
  • Choose between top-level code in the last file and an explicit [<EntryPoint>] function

Before you start

What you’ll learn · Before you start

What you will learn

  • Create console, library and test projects with dotnet new and explain the Compile order of an .fsproj
  • Fix a 'not defined' error caused by file order instead of reaching for a recursive module
  • Read F# diagnostics by their codes, such as FS0001, FS0039, FS0025 and FS0020
  • Configure warnings for a few lines with
  • Choose between top-level code in the last file and an explicit [<EntryPoint>] function

Before you start

On this page

A new F# console project has one source file and builds in seconds. Add two more files and list them in the wrong order, and the build stops with error FS0039 although every name is spelled right, because F# compiles files in the order the project lists them and a file can only use what came before it.

This lesson creates the three kinds of project you will use most, reads a project file, breaks and fixes its file order, reads the compiler’s messages by their codes, controls warnings with the directives F# 10 introduced, and gives a program an entry point with an exit status.

dotnet new writes a project file that lists every source file

dotnet new creates a project from a template, and -lang "F#" picks the F# version of it. This session makes a console app, a class library and an xUnit test project, then runs the app:

Three projects from dotnet new, and the console app's build output Python · new_session.py
from session import run

# Three F# projects from the SDK's templates: a console app, a class library and a test project (made without
# restoring its packages, which would need the network); the line of the SDK's F# settings that stops F# projects
# from compiling every source file in their folder; then the console app's build output, with and without
# FSharp.Core.dll.
run("""
dotnet new console -lang "F#" -o Shop
cat Shop/Shop.fsproj
cat Shop/Program.fs
grep "Do not glob" "$DOTNET_ROOT/sdk/10.0.401/FSharp/Microsoft.FSharp.NetSdk.props" | tr -s ' '
dotnet new classlib -lang "F#" -o Pricing
cat Pricing/Pricing.fsproj
dotnet new xunit -lang "F#" -o Pricing.Tests --no-restore
cat Pricing.Tests/Pricing.Tests.fsproj
dotnet run --project Shop
ls Shop/bin/Debug/net10.0
rm Shop/bin/Debug/net10.0/FSharp.Core.dll
!dotnet Shop/bin/Debug/net10.0/Shop.dll
""")

Output

$ dotnet new console -lang "F#" -o Shop
The template "Console App" was created successfully.

Processing post-creation actions...
Restoring Shop/Shop.fsproj:
  Determining projects to restore...
  Restored Shop/Shop.fsproj (in …).
Restore succeeded.
$ cat Shop/Shop.fsproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>
$ cat Shop/Program.fs
// For more information see https://aka.ms/fsharp-console-apps
printfn "Hello from F#"
$ grep "Do not glob" "$DOTNET_ROOT/sdk/10.0.401/FSharp/Microsoft.FSharp.NetSdk.props" | tr -s ' '
 <EnableDefaultCompileItems Condition=" '$(EnableDefaultCompileItems)' == '' ">false</EnableDefaultCompileItems> <!-- Do not glob F# source files -->
 <EnableDefaultNoneItems Condition=" '$(EnableDefaultNoneItems)' == '' ">false</EnableDefaultNoneItems> <!-- Do not glob F# none items-->
$ dotnet new classlib -lang "F#" -o Pricing
The template "Class Library" was created successfully.

Processing post-creation actions...
Restoring Pricing/Pricing.fsproj:
  Determining projects to restore...
  Restored Pricing/Pricing.fsproj (in …).
Restore succeeded.
$ cat Pricing/Pricing.fsproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Library.fs" />
  </ItemGroup>

</Project>
$ dotnet new xunit -lang "F#" -o Pricing.Tests --no-restore
The template "xUnit Test Project" was created successfully.
$ cat Pricing.Tests/Pricing.Tests.fsproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Tests.fs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.4" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
    <PackageReference Include="xunit" Version="2.9.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
  </ItemGroup>

</Project>
$ dotnet run --project Shop
Hello from F#
$ ls Shop/bin/Debug/net10.0
cs
de
es
fr
FSharp.Core.dll
it
ja
ko
pl
pt-BR
ru
Shop
Shop.deps.json
Shop.dll
Shop.pdb
Shop.runtimeconfig.json
tr
zh-Hans
zh-Hant
$ rm Shop/bin/Debug/net10.0/FSharp.Core.dll
$ dotnet Shop/bin/Debug/net10.0/Shop.dll
Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'FSharp.Core, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. The system cannot find the file specified.

File name: 'FSharp.Core, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
   at <StartupCode$Shop>.$Program.main@()
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 new_session.py

Each project file: An F# project's .fsproj file: an MSBuild file that names the target framework, the package references and, as Compile items, every source file in the order the compiler reads them. is short. The console app’s says it is an executable for net10.0 and lists one Compile item, Program.fs; the library has Library.fs and asks for an XML documentation file; the test project lists Tests.fs and four package references, among them xunit 2.9.3. A C# project compiles every .cs file in its folder by default; the first line of the SDK’s F# settings that the session printed switches that off for F#, with the comment “Do not glob F# source files”. An F# project compiles exactly the files its Compile items list, in that order, so a new file in the folder is ignored until you add it to the list.

The console app’s Program.fs is two lines with no main: its top-level code: Statements written directly in the last file of an F# application, outside any function. They run from top to bottom when the program starts: the implicit entry point. runs from top to bottom when the program starts. Its build output holds the program, Shop.dll, and beside it FSharp.Core.dll and thirteen folders of FSharp.Core’s localized resources, one for each language. Delete FSharp.Core.dll and the program cannot start: the runtime stops with FileNotFoundException before the first line runs, which is why the FSharp.Core notes say to always deploy it with a compiled application.

Command Source file it makes What it builds
dotnet new console -lang "F#" Program.fs, top-level code a console app
dotnet new classlib -lang "F#" Library.fs, a module a class library, .dll
dotnet new xunit -lang "F#" Tests.fs a test project

The test template's xUnit version

The F# xUnit template of SDK 10.0.401 still references xunit 2.9.3, a 2.x version, as the recorded project file shows. This track’s testing module uses xUnit v3; check the package versions of any template before you build on it.

A file can use only what the files above it define

Here is a small shop program in three files. Domain.fs defines the record type Item, Pricing.fs totals an order of items, and Program.fs prints a total:

The shop program, in the order that works

order-right/Domain.fs

module Shop.Domain

type Item = { Name: string; Price: decimal; Qty: int }

order-right/Pricing.fs

module Shop.Pricing

open Shop.Domain

let lineTotal item = item.Price * decimal item.Qty

let orderTotal items = items |> List.sumBy lineTotal

order-right/Program.fs

open Shop.Domain
open Shop.Pricing

let order =
    [ { Name = "Pen"; Price = 25m; Qty = 4 }
      { Name = "Pad"; Price = 60m; Qty = 2 } ]

printfn "Total: %M" (orderTotal order)

order-right/Orders.fsproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Domain.fs" />
    <Compile Include="Pricing.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

The same three files went into four projects that differ only in their Compile order and, once, a rec keyword:

One program, four file orders Python · order_session.py
from session import run

# The same three files in four projects: Pricing.fs before Domain.fs, the same with Pricing a rec module, the order
# that works, and Program.fs first. -v q (quiet) leaves only the messages, each printed when it happens and again in
# MSBuild's summary.
run("""
!dotnet build order-wrong -v q
!dotnet build order-rec -v q
dotnet build order-right -v q
dotnet run --project order-right
!dotnet build program-first -v q
""")

Output

$ dotnet build order-wrong -v q
order-wrong/Pricing.fs(3,6): error FS0039: The namespace or module 'Shop' is not defined. [order-wrong/Orders.fsproj]
order-wrong/Pricing.fs(5,22): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [order-wrong/Orders.fsproj]
order-wrong/Pricing.fs(5,43): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [order-wrong/Orders.fsproj]

Build FAILED.

order-wrong/Pricing.fs(3,6): error FS0039: The namespace or module 'Shop' is not defined. [order-wrong/Orders.fsproj]
order-wrong/Pricing.fs(5,22): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [order-wrong/Orders.fsproj]
order-wrong/Pricing.fs(5,43): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [order-wrong/Orders.fsproj]
    0 Warning(s)
    3 Error(s)

Time Elapsed …
[exit status 1]
$ dotnet build order-rec -v q
order-rec/Pricing.fs(3,11): error FS0039: The namespace 'Domain' is not defined. [order-rec/Orders.fsproj]
order-rec/Pricing.fs(3,11): error FS0039: The namespace 'Domain' is not defined. [order-rec/Orders.fsproj]
order-rec/Pricing.fs(3,11): error FS0039: The namespace 'Domain' is not defined. [order-rec/Orders.fsproj]
order-rec/Pricing.fs(5,22): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [order-rec/Orders.fsproj]
order-rec/Pricing.fs(5,43): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [order-rec/Orders.fsproj]

Build FAILED.

order-rec/Pricing.fs(3,11): error FS0039: The namespace 'Domain' is not defined. [order-rec/Orders.fsproj]
order-rec/Pricing.fs(3,11): error FS0039: The namespace 'Domain' is not defined. [order-rec/Orders.fsproj]
order-rec/Pricing.fs(3,11): error FS0039: The namespace 'Domain' is not defined. [order-rec/Orders.fsproj]
order-rec/Pricing.fs(5,22): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [order-rec/Orders.fsproj]
order-rec/Pricing.fs(5,43): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [order-rec/Orders.fsproj]
    0 Warning(s)
    5 Error(s)

Time Elapsed …
[exit status 1]
$ dotnet build order-right -v q

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet run --project order-right
Total: 220
$ dotnet build program-first -v q
program-first/Program.fs(1,1): error FS0222: Files in libraries or multiple-file applications must begin with a namespace or module declaration, e.g. 'namespace SomeNamespace.SubNamespace' or 'module SomeNamespace.SomeModule'. Only the last source file of an application may omit such a declaration. [program-first/Orders.fsproj]

Build FAILED.

program-first/Program.fs(1,1): error FS0222: Files in libraries or multiple-file applications must begin with a namespace or module declaration, e.g. 'namespace SomeNamespace.SubNamespace' or 'module SomeNamespace.SomeModule'. Only the last source file of an application may omit such a declaration. [program-first/Orders.fsproj]
    0 Warning(s)
    1 Error(s)

Time Elapsed …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_session.py

In order-wrong, Pricing.fs comes before Domain.fs, and the build fails with FS0039, “The namespace or module ‘Shop’ is not defined”, followed by two FS0072 errors that are only consequences: without Item, the compiler cannot tell what item.Price means. Making Pricing a rec module in order-rec does not help: the build still fails with FS0039, because rec only lets the declarations inside one module refer to each other. In the right order the program builds and prints Total: 220. And with Program.fs first, the build stops with FS0222: only the last file of an application may leave out its module declaration. Step through what the compiler sees in the right order and in the swapped one:

Interactive · step by step The compiler reads the files in the order the project lists them Step through the right order, then the swapped one. An arrow points from a file to a file it uses.
The compiler reads the files in the order the project lists them, step 6 of 6. Showing Orders.fsproj (Domain, Pricing, Program), Domain.fs (module Shop.Domain), Pricing.fs (module Shop.Pricing), Program.fs. In focus: Domain.fs, Pricing.fs, Program.fs.opensopensOrders.fsprojDomain, Pricing, ProgramDomain.fsmodule Shop.DomainPricing.fsmodule Shop.PricingProgram.fstop-level code, last

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The order is in the project. The project file lists the source files as Compile items, and the compiler reads them in exactly that order, from the first item to the last.
  2. Domain.fs first. Domain.fs defines the module Shop.Domain and its record type Item. Nothing comes before it, so it can use only FSharp.Core and .NET.
  3. Pricing.fs sees Domain. Pricing.fs opens Shop.Domain, which the compiler has already read, so item.Price and item.Qty have a known type.
  4. Program.fs comes last. Program.fs opens both modules and calls orderTotal from Pricing.fs. As the last file of an application it needs no module declaration, and its top-level code is what the program runs.
  5. Swap the first two. In order-wrong the project lists Pricing.fs before Domain.fs. Now Pricing.fs is read first, and Shop.Domain does not exist yet.
  6. Fix the order, not the code. The cure is the order. List each file after the files it uses, so every arrow points upwards; making Pricing a rec module does not help, because rec never lets a file see a later one.

Fixing the order with rec

Wrong: making a module or namespace rec until the error goes away. Right: move the file in the project’s Compile list so that it comes after every file it uses. rec is for types and functions in one file that refer to each other, and it cannot make a file see a later one.

The rule is deliberate. Because a file sees only the files above it, dependencies between files can point one way only, and the list in the project file reads as the program’s layers, from the domain types at the top to the program at the bottom. No file can depend on a later one, so files can never form a cycle.

Key idea

F# compiles the files in the order of the project’s Compile items, and each file sees only the files above it: fix a “not defined” error between files by moving the file, never by adding rec.

Checkpoint

Check: What dotnet new writes · File order in a project

  1. Question 1 of 2 You create Discounts.fs in the Shop folder and call its function from Program.fs. The build says the module Discounts is not defined. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The file is not one of the project's Compile items, so the compiler never reads it

    An F# project compiles the files its Compile items list, in order. Add Discounts.fs to the list, above Program.fs, and the module exists for every file below it.

  2. Question 2 of 2 In order-wrong, Pricing.fs comes before Domain.fs and the build fails with FS0039. Which change makes it succeed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Move Domain.fs above Pricing.fs in the project's Compile list

    Each file sees only the files above it. A not-defined error between files is fixed by moving the file that uses a name below the file that defines it.

The FS code says what kind of problem the compiler found

Every F# compiler message has a code, FS and four digits, and the code tells you more than the text: the same code always means the same kind of problem. Two projects show the ones you will meet first. warnings builds, with three warnings, and errors does not build at all:

Two files that the compiler complains about

warnings/Rules.fs · warnings/Rules.fs

module Rules

type Tier =
    | Standard
    | Member
    | Partner

let rate tier =
    match tier with
    | Standard -> 0m
    | Member -> 0.05m

let label tier =
    match tier with
    | Standard -> "standard"
    | _ -> "other"
    | Member -> "member"

let report (prices: decimal list) =
    List.map (fun p -> p * 2m) prices
    printfn "checked %d prices" prices.Length

errors/Broken.fs · errors/Broken.fs

module Broken

let total = 2 + 1.5

let describe n =
    if n > 0 then "positive"

let shout name = name.ToUpper()

let missing = totl
Three warnings, then five errors Python · diagnostics_session.py
from session import run

# One project that builds with three warnings, and one that fails with five errors.
run("""
dotnet build warnings -v q
!dotnet build errors -v q
""")

Output

$ dotnet build warnings -v q
warnings/Rules.fs(9,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'Partner' may indicate a case not covered by the pattern(s). [warnings/Warnings.fsproj]
warnings/Rules.fs(17,7): warning FS0026: This rule will never be matched [warnings/Warnings.fsproj]
warnings/Rules.fs(20,5): warning FS0020: The result of this expression has type 'decimal list' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'. [warnings/Warnings.fsproj]

Build succeeded.

warnings/Rules.fs(9,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'Partner' may indicate a case not covered by the pattern(s). [warnings/Warnings.fsproj]
warnings/Rules.fs(17,7): warning FS0026: This rule will never be matched [warnings/Warnings.fsproj]
warnings/Rules.fs(20,5): warning FS0020: The result of this expression has type 'decimal list' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'. [warnings/Warnings.fsproj]
    3 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet build errors -v q
errors/Broken.fs(3,17): error FS0001: The type 'float' does not match the type 'int' [errors/Errors.fsproj]
errors/Broken.fs(3,15): error FS0043: The type 'float' does not match the type 'int' [errors/Errors.fsproj]
errors/Broken.fs(6,19): error FS0001: This 'if' expression is missing an 'else' branch. Because 'if' is an expression, and not a statement, add an 'else' branch which also returns a value of type 'string'. [errors/Errors.fsproj]
errors/Broken.fs(8,18): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [errors/Errors.fsproj]
errors/Broken.fs(10,15): error FS0039: The value or constructor 'totl' is not defined. Maybe you want one of the following:   total [errors/Errors.fsproj]

Build FAILED.

errors/Broken.fs(3,17): error FS0001: The type 'float' does not match the type 'int' [errors/Errors.fsproj]
errors/Broken.fs(3,15): error FS0043: The type 'float' does not match the type 'int' [errors/Errors.fsproj]
errors/Broken.fs(6,19): error FS0001: This 'if' expression is missing an 'else' branch. Because 'if' is an expression, and not a statement, add an 'else' branch which also returns a value of type 'string'. [errors/Errors.fsproj]
errors/Broken.fs(8,18): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point. [errors/Errors.fsproj]
errors/Broken.fs(10,15): error FS0039: The value or constructor 'totl' is not defined. Maybe you want one of the following:   total [errors/Errors.fsproj]
    0 Warning(s)
    5 Error(s)

Time Elapsed …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 diagnostics_session.py

Each message starts with the file and the line and column, Rules.fs(9,11), then warning or error and the code. MSBuild prints each message twice: once as the compiler reports it and again in the summary at the end.

Code What it means, and the case above The usual fix
FS0001 two types that must agree do not: 2 + 1.5, an if without else convert explicitly, or add the branch
FS0039 a name is not defined at this point: totl, or a file listed too late fix the spelling, the open or the file order
FS0072 a member is used before the type is known: name.ToUpper() annotate the parameter, (name: string)
FS0025 a match misses a case: rate has no rule for Partner add the case, or _ when that is truly right
FS0026 a rule can never match: Member after _ move it above the catch-all, or delete it
FS0020 a result is silently thrown away: the unused List.map use the result, or pipe it to ignore

The compiler’s own text often contains the fix. The FS0001 for the if explains that if is an expression, not a statement, so without an else it cannot produce a string, and the FS0039 for totl suggests total. Read the first error first: the second error of 2 + 1.5, FS0043, repeats the first, as the FS0072 errors after a missing module did above.

Checkpoint

Check: Reading FS codes

  1. Question 1 of 2 Rules.fs line 20 calls List.map (fun p -> p * 2m) prices, and nothing uses the result. What does FS0020 tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The new list is thrown away; use it, or pipe it to ignore if that really is the intent

    F# warns whenever a value other than unit is silently discarded, because that is usually a bug: here the doubled prices were computed and lost.

  2. Question 2 of 2 Why does let shout name = name.ToUpper() fail with FS0072, and what fixes it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The compiler does not know name's type when it reaches .ToUpper(); annotate it, (name: string)

    F# infers types in the order it reads the code. When a member is used on a value whose type is not yet known, say the type once, and everything after it is inferred again.

Warnings can be silenced for a few lines, or made errors for a project

A warning that you have decided is acceptable should be silenced exactly where it applies, and no further. In F# 10, #nowarn turns a warning off until a matching #warnon or the end of the file; before F# 10 a #nowarn lasted to the end of the file, and in scripts it applied to the whole compilation. Here the incomplete match in rate is deliberate and the one in label is not:

scoped/Tiers.fs: one deliberate incomplete match FS · scoped/Tiers.fs
module Tiers

type Tier =
    | Standard
    | Member
    | Partner

// Partner is priced elsewhere, so this incomplete match is deliberate.
#nowarn 25
let rate tier =
    match tier with
    | Standard -> 0m
    | Member -> 0.05m
#warnon 25

let label tier =
    match tier with
    | Standard -> "standard"
    | Member -> "member"
Scoped warnings, an opt-in warning, and warnings as errors Python · warnings_session.py
from session import run

# Warning controls: a #nowarn/#warnon pair (F# 10, then the same file compiled as F# 9), an opt-in warning for
# unused values, and every warning treated as an error. --no-incremental compiles again although no file changed.
run("""
dotnet build scoped -v q
!dotnet build scoped -v q --no-incremental -p:LangVersion=9.0
dotnet build warnings -v q --no-incremental -p:WarnOn=1182
!dotnet build warnings -v q --no-incremental -p:TreatWarningsAsErrors=true
""")

Output

$ dotnet build scoped -v q
scoped/Tiers.fs(17,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'Partner' may indicate a case not covered by the pattern(s). [scoped/Tiers.fsproj]

Build succeeded.

scoped/Tiers.fs(17,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'Partner' may indicate a case not covered by the pattern(s). [scoped/Tiers.fsproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet build scoped -v q --no-incremental -p:LangVersion=9.0
scoped/Tiers.fs(14,1): error FS3350: Feature 'Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules' is not available in F# 9.0. Please use language version 10.0 or greater. [scoped/Tiers.fsproj]

Build FAILED.

scoped/Tiers.fs(14,1): error FS3350: Feature 'Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules' is not available in F# 9.0. Please use language version 10.0 or greater. [scoped/Tiers.fsproj]
    0 Warning(s)
    1 Error(s)

Time Elapsed …
[exit status 1]
$ dotnet build warnings -v q --no-incremental -p:WarnOn=1182
warnings/Rules.fs(9,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'Partner' may indicate a case not covered by the pattern(s). [warnings/Warnings.fsproj]
warnings/Rules.fs(17,7): warning FS0026: This rule will never be matched [warnings/Warnings.fsproj]
warnings/Rules.fs(20,5): warning FS0020: The result of this expression has type 'decimal list' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'. [warnings/Warnings.fsproj]
warnings/Program.fs(3,9): warning FS1182: The value 'unused' is unused [warnings/Warnings.fsproj]

Build succeeded.

warnings/Rules.fs(9,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'Partner' may indicate a case not covered by the pattern(s). [warnings/Warnings.fsproj]
warnings/Rules.fs(17,7): warning FS0026: This rule will never be matched [warnings/Warnings.fsproj]
warnings/Rules.fs(20,5): warning FS0020: The result of this expression has type 'decimal list' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'. [warnings/Warnings.fsproj]
warnings/Program.fs(3,9): warning FS1182: The value 'unused' is unused [warnings/Warnings.fsproj]
    4 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet build warnings -v q --no-incremental -p:TreatWarningsAsErrors=true
warnings/Rules.fs(9,11): error FS0025: Incomplete pattern matches on this expression. For example, the value 'Partner' may indicate a case not covered by the pattern(s). [warnings/Warnings.fsproj]
warnings/Rules.fs(17,7): error FS0026: This rule will never be matched [warnings/Warnings.fsproj]
warnings/Rules.fs(20,5): error FS0020: The result of this expression has type 'decimal list' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'. [warnings/Warnings.fsproj]

Build FAILED.

warnings/Rules.fs(9,11): error FS0025: Incomplete pattern matches on this expression. For example, the value 'Partner' may indicate a case not covered by the pattern(s). [warnings/Warnings.fsproj]
warnings/Rules.fs(17,7): error FS0026: This rule will never be matched [warnings/Warnings.fsproj]
warnings/Rules.fs(20,5): error FS0020: The result of this expression has type 'decimal list' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'. [warnings/Warnings.fsproj]
    0 Warning(s)
    3 Error(s)

Time Elapsed …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 warnings_session.py

Only label’s FS0025 is reported: the pair of directives silenced the warning for rate alone. Compiled as F# 9, the same file fails with FS3350, because the feature needs language version 10.0, a reminder that LangVersion changes what a file may contain. The F# 10 notes list the stricter rules that came with it: no space between # and nowarn, and no multi-line or empty warn directives.

Two project settings act on every file. <WarnOn>1182</WarnOn> switches on an opt-in warning, here FS1182 for the unused value in main; the compiler options page lists the other opt-in warnings. <TreatWarningsAsErrors>true</TreatWarningsAsErrors> turns every warning into an error, so the build that succeeded with three warnings fails with three errors. Set it in the build that checks every change, and warnings cannot pile up unnoticed.

One more difference matters when you try diagnostics in a script. [<TailCall>] asks the compiler to check that a function’s recursive calls are tail calls, and sumTo adds after its call returns:

tailcall/Sum.fs: a function that is not tail recursive FS · tailcall/Sum.fs
module Sum

// [<TailCall>] asks the compiler to check that every recursive call is a tail call. This one is not:
// the addition happens after the call returns.
[<TailCall>]
let rec sumTo n =
    if n = 0 then 0 else n + sumTo (n - 1)
The same function, built and run as a script Python · tailcall_session.py
from session import run

# The same non-tail-recursive function with [<TailCall>], compiled by dotnet build and run as a script.
run("""
dotnet build tailcall -v q
dotnet fsi --nologo tailcall/sum.fsx
""")

Output

$ dotnet build tailcall -v q
tailcall/Sum.fs(7,30): warning FS3569: The member or function 'sumTo' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. [tailcall/Sum.fsproj]

Build succeeded.

tailcall/Sum.fs(7,30): warning FS3569: The member or function 'sumTo' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. [tailcall/Sum.fsproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet fsi --nologo tailcall/sum.fsx
5050

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tailcall_session.py

dotnet build reports FS3569, and dotnet fsi runs the same code as a script and prints 5050 without a word: F# Interactive does not run every check that a build runs. Check warnings with dotnet build before you trust them to be absent.

Checkpoint

Check: Controlling warnings

  1. Question 1 of 2 In scoped/Tiers.fs, #nowarn 25 and #warnon 25 surround rate, and label is below them. Which FS0025 warnings does F# 10 report?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only the one for label

    F# 10 scopes warn directives: #nowarn turns a warning off until a matching #warnon or the end of the file, so a deliberate exception stays exactly as wide as it should be.

  2. Question 2 of 2 Your team wants the build that checks every change to fail on any warning, while local builds may still pass with warnings. What do you set?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: TreatWarningsAsErrors=true in that build, for example dotnet build -p:TreatWarningsAsErrors=true

    TreatWarningsAsErrors turns every warning into an error. Set it in the shared build, and add WarnOn for the opt-in warnings the team wants checked too.

The program starts in the last file, at the top or at main

An F# application starts in its last file in one of two ways. With top-level code, as in the console template, the statements of the last file run from top to bottom. With an explicit entry point: A function marked [<EntryPoint>] of type string array -> int, the last declaration of the last file. It receives the command-line arguments, and its result is the exit status., a function marked [<EntryPoint>] of type string array -> int receives the command-line arguments, and the number it returns is the program’s exit status:

greet/Program.fs: an entry point with an exit status FS · greet/Program.fs
// The entry point: the last declaration of the last file. Its int result is the exit status of the process.
[<EntryPoint>]
let main args =
    match args with
    | [| name |] ->
        printfn "Hello, %s" name
        0
    | _ ->
        eprintfn "usage: greet <name>"
        64
Two runs of greet, then an entry point in the wrong place Python · entry_session.py
from session import run

# An explicit entry point whose result is the exit status, then an entry point that is not in the last file.
run("""
dotnet run --project greet -- Asha
!dotnet run --project greet
!dotnet build entry-early -v q
""")

Output

$ dotnet run --project greet -- Asha
Hello, Asha
$ dotnet run --project greet
usage: greet <name>
[exit status 64]
$ dotnet build entry-early -v q
entry-early/Program.fs(4,5): error FS0433: A function labeled with the 'EntryPointAttribute' attribute must be the last declaration in the last file in the compilation sequence. [entry-early/Early.fsproj]

Build FAILED.

entry-early/Program.fs(4,5): error FS0433: A function labeled with the 'EntryPointAttribute' attribute must be the last declaration in the last file in the compilation sequence. [entry-early/Early.fsproj]
    0 Warning(s)
    1 Error(s)

Time Elapsed …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 entry_session.py

With one argument, greet printed its greeting and returned 0; with none it wrote the usage line to standard error and returned 64, which the shell reported as the exit status. The args array holds only the arguments, without the program’s name. The last build shows the rule’s other half: in entry-early, the function marked [<EntryPoint>] is in the first of two files, and the build fails with FS0433, because the entry point must be the last declaration in the last file.

Top-level code [<EntryPoint>]
Where the statements of the last file the last declaration of the last file
Arguments System.Environment.GetCommandLineArgs() the args parameter
Exit status exit n, or 0 at the end the function’s int result
Suits small programs and the template programs whose main you want to test

Microsoft’s guide to explicit entry points gives the three reasons to choose one: arguments as a parameter, an exit code as a return value, and a main that a test can call.

Checkpoint

Check: Where the program starts

  1. One question greet's main returns 64 when it gets no argument. What does the shell, or a build step that runs greet, see?

    Choose one answer.

    Show the answer: One question

    Answer: Exit status 64

    With an explicit entry point, returning a number is how an F# program reports success or failure: 0 for success, and any other value the program documents.

Practise: projects, order and messages

These questions draw on the sessions above: file orders, codes, warning controls and entry points. The idea you have practised least is asked about first, and each answer explains itself as soon as you pick it:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What decides which files an F# project compiles, and in which order?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: The Compile items of its .fsproj, in the order they are listed

    The .fsproj's Compile list is both the set of files and their order, which is why moving a file in that list fixes ordering errors.

  2. Question 2 of 10 Shop's build output held Shop.dll and FSharp.Core.dll. What happened when FSharp.Core.dll was deleted and Shop.dll run?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: It stopped with FileNotFoundException for FSharp.Core before its first line ran

    An F# application ships FSharp.Core.dll beside itself, as the FSharp.Core notes require. Deploy the whole build output, not just the program's own .dll.

  3. Question 3 of 10 Program.fs, which starts with open declarations and has no module declaration, is listed first of three files. What does the build report?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: FS0222, because only the last file of an application may leave out its module declaration

    A file without a module or namespace declaration becomes an implicit module, which only the last file of an application may be. Give every other file its module declaration.

  4. Question 4 of 10 A C# developer asks why F# makes file order matter at all. Which answer is right?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Because each file sees only earlier files, dependencies between files point one way, so files can never form a cycle

    The rule trades a little freedom for structure: you always know that a file depends only on what is above it, and cycles between files cannot form.

  5. Question 5 of 10 The build reported FS0001 for if n > 0 then "positive" with no else. What does it mean?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: Without an else, the if cannot produce a string when the condition is false

    Every F# if is an expression with a value, so both branches must produce a value of the same type, unless that type is unit.

  6. Question 6 of 10 A build prints FS0039 for an open declaration, then twenty FS0072 errors in the same file. Where do you start?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: With the FS0039, because the FS0072 errors follow from the missing module

    One missing name can make the compiler lose track of every type that depends on it. Read and fix the first error, then build again before reading the rest.

  7. Question 7 of 10 How do you switch on the compiler's warning for unused values in a project?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Add <WarnOn>1182</WarnOn> to the project file

    Some warnings are off unless you ask for them. The compiler options page lists them, and WarnOn takes their numbers, separated by commas.

  8. Question 8 of 10 The tailcall script printed 5050 without a warning, and dotnet build of the same function reported FS3569. What do you conclude?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: F# Interactive does not run every check a build runs, so confirm warnings with dotnet build

    Scripts are a fast way to try code, but a project build is the check that counts for diagnostics, as this track's own examples are verified by building.

  9. Question 9 of 10 Where must the function marked [<EntryPoint>] be?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: It must be the last declaration in the last file of the project

    The program starts in its last file, whether with top-level code or with an [<EntryPoint>] function, which must be the very last declaration there.

  10. Question 10 of 10 You want a unit test for the code that turns command-line arguments into an exit status. Which form of entry point helps, and why?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: [<EntryPoint>] main args, because a test can call main with an array and check the int it returns

    An explicit entry point is an ordinary function from arguments to an exit status, so it can be tested like any other function, while top-level code runs only once, when the program starts.

Interview questions

Warm-up (fresher to 2–5 years): why does the order of files in an F# project matter, and what is the rule?

F# compiles the files of a project in the order of the project file’s Compile items, and each file can use only what the files before it define, so a file that uses a type from a later file fails with FS0039, “not defined”. The order is part of the design: dependencies between files point one way, from later files to earlier ones, so a project cannot contain a cycle between files, and the list reads as the program’s layers. The fix for an ordering error is to move the file in the project file’s list, not to make modules recursive: rec lets the declarations inside one module refer to each other, but never lets a file see a later one. In an application, the last file is where the program starts.

Two harder questions follow, about a build that breaks when a file is added and about a team’s warnings:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A teammate adds Discounts.fs to a working F# project and moves some pricing code into it. The build now fails with FS0039 in two files and a dozen FS0072 errors. Walk through how you diagnose and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your team's F# solution builds with 140 warnings that everyone has learned to ignore. Propose a warnings policy, and explain how you would get from 140 warnings to it without stopping feature work.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • dotnet new console, classlib and xunit with -lang "F#" make projects whose .fsproj lists every source file as a Compile item; a built application ships FSharp.Core.dll beside it.
  • F# compiles files in the project’s order and a file sees only the files above it, so “not defined” errors between files are fixed by moving the file, never with rec.
  • An FS code names the kind of problem: FS0001 types that disagree, FS0039 a missing name, FS0025 an incomplete match, FS0020 a discarded result; read the first error first.
  • In F# 10, #nowarn lasts until a matching #warnon, WarnOn enables opt-in warnings, TreatWarningsAsErrors stops the build on any warning, and only a build reports some warnings, such as FS3569.
  • An application starts in its last file, with top-level code or with an [<EntryPoint>] function whose int result is the exit status.

Terms in this lesson

entry point
A function marked [<EntryPoint>] of type string array -> int, the last declaration of the last file. It receives the command-line arguments, and its result is the exit status.
project file
An F# project's .fsproj file: an MSBuild file that names the target framework, the package references and, as Compile items, every source file in the order the compiler reads them.
top-level code
Statements written directly in the last file of an F# application, outside any function. They run from top to bottom when the program starts: the implicit entry point.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.