100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

F# Best Practices

Guidelines for writing idiomatic, maintainable F# code covering function composition, project organization, and error handling.

PracticeIntermediate9 min readJul 10, 2026
Analogies

Writing Idiomatic F#

Idiomatic F# favors immutability, small composable functions, and pipeline-style data transformation using the |> operator over deeply nested function calls. Prefer expressions that return a value over statements with side effects, and let the type system encode invariants - through records and discriminated unions - rather than adding scattered runtime checks. Code that reads as a left-to-right pipeline of small steps is both easier to test in isolation and easier to reason about than one large function doing everything.

🏏

Cricket analogy: Like building an innings around rotating strike with quick singles (small composable functions) rather than swinging for a six on every ball (one giant function): a batter such as Steve Smith accumulates runs safely through many small, well-placed shots piped together over after over.

Naming, Modules, and Project Organization

F# compiles files in the order they are listed in the .fsproj, so file order directly encodes the dependency graph: a file can only reference types and functions defined in files listed earlier. Organize code into modules with clear, verb-first function names, and keep domain types - records and discriminated unions - near the top of that graph so business rules stay decoupled from I/O and infrastructure concerns, which should live in files compiled later.

🏏

Cricket analogy: Like a batting order where the top-order openers must be settled before the middle order can build an innings around them: F# file order works the same way, foundational domain types must compile before the functions that depend on them.

Error Handling and Testing

Prefer Result<'T, 'TError> and Option<'T> for expected failure paths instead of exceptions, reserving exceptions for truly exceptional, unrecoverable conditions such as programmer errors or I/O infrastructure failure. Write unit tests with Expecto or xUnit paired with FsCheck for property-based testing, which is especially effective at catching edge cases in pure functions, since F# functions with no hidden dependencies are naturally easy to test in isolation.

🏏

Cricket analogy: Like using DRS reviews (Result type) for genuinely reviewable decisions rather than a player storming off the field in protest (uncontrolled exception): a controlled review process, used sparingly and correctly the way MS Dhoni was famous for, keeps the game orderly.

fsharp
type ValidationError = EmptyName | NegativeAge

let validateAge age =
    if age < 0 then Error NegativeAge else Ok age

let validateName name =
    if System.String.IsNullOrWhiteSpace name then Error EmptyName else Ok name

let createPerson name age =
    validateName name
    |> Result.bind (fun n ->
        validateAge age
        |> Result.map (fun a -> {| Name = n; Age = a |}))

// Usage: pipeline style, no exceptions for expected validation failures
match createPerson "Ada" 36 with
| Ok person -> printfn "Created: %A" person
| Error EmptyName -> printfn "Name cannot be empty"
| Error NegativeAge -> printfn "Age cannot be negative"

The F# style guide, maintained by the F# Software Foundation, favors camelCase for values and functions, PascalCase for types, modules, and discriminated union cases, and 4-space indentation with no tabs, since indentation is semantically significant.

Overusing mutable state or wrapping everything in classes defeats F#'s strengths. If a codebase reads like C# translated line-by-line into F# syntax, it's a signal the design isn't leveraging immutability, pattern matching, or the pipeline operator effectively.

  • Prefer small, composable functions connected with the pipeline operator (|>) over deeply nested calls.
  • Let file order in the .fsproj encode the dependency graph: domain types first, infrastructure and I/O last.
  • Use PascalCase for types/modules/DU cases and camelCase for values/functions per the F# style guide.
  • Model expected failures with Option<'T> and Result<'T,'TError> instead of throwing exceptions.
  • Reserve exceptions for truly exceptional, unrecoverable conditions, not routine validation failures.
  • Use property-based testing (FsCheck) alongside example-based tests (Expecto/xUnit) since pure functions are easy to test exhaustively.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#FStudyNotes#FBestPractices#Writing#Idiomatic#Naming#Modules#StudyNotes#SkillVeris#ExamPrep