Skip to content
AgileLogics
6 min read

Options: No More null

Prerequisites: Pattern matching basics

Many languages use null for "no value", and forgetting to check for it is one of the most common bugs in software. F# uses the option type instead: a value is either Some x or None, and the type (int option, string option) tells you it might be missing.

let tryDivide a b =
    if b = 0 then None
    else Some (a / b)

printfn "%A" (tryDivide 10 2)
printfn "%A" (tryDivide 10 0)
Some 5
None

Using an option

You can't use an int option as an int by accident — you have to handle both cases. The clearest way is match:

let tryDivide a b = if b = 0 then None else Some (a / b)

let report a b =
    match tryDivide a b with
    | Some result -> printfn "%d / %d = %d" a b result
    | None -> printfn "%d / %d: can't divide by zero" a b

report 10 2
report 10 0
10 / 2 = 5
10 / 0: can't divide by zero

The Option module

For common cases, the Option module is shorter than a match:

let price = Some 20
let missing: int option = None

printfn "%A" (price |> Option.map (fun p -> p * 2))
printfn "%A" (missing |> Option.map (fun p -> p * 2))
printfn "%d" (price |> Option.defaultValue 0)
printfn "%d" (missing |> Option.defaultValue 0)
printfn "%b %b" price.IsSome missing.IsNone
printfn "%A" (price |> Option.filter (fun p -> p > 50))
Some 40
None
20
0
true true
None
  • Option.map transforms the value if there is one, and leaves None alone.
  • Option.defaultValue unwraps with a fallback.
  • Option.bind chains steps that each return an option.

Options everywhere

The standard library returns options wherever something might not be found:

let tryParseInt (s: string) =
    match System.Int32.TryParse s with
    | true, n -> Some n
    | false, _ -> None

let inputs = ["42"; "oops"; "7"]
let valid = inputs |> List.choose tryParseInt

printfn "%A" (inputs |> List.map tryParseInt)
printfn "%A" valid
printfn "%A" (List.tryHead ([]: int list))
printfn "%A" ([3; 9; 4] |> List.tryFind (fun n -> n > 5))
[Some 42; None; Some 7]
[42; 7]
None
Some 9

List.choose keeps only the Some values and unwraps them — perfect for "parse what you can".

Common mistakes

  • Using .Value on an option (price.Value) — it throws on None, bringing back exactly the bug options prevent. Use match or Option.defaultValue.
  • Returning null from your own F# functions — return an option instead. (You'll still meet null from .NET libraries; Level 3 covers how to handle it.)

Practice

Write tryFindUser id that looks up an id in the list [(1, "Ada"); (2, "Grace")] and returns an option, then print "Hello, Ada" for id 1 and "Unknown user" for id 3.

Show solution
let users = [(1, "Ada"); (2, "Grace")]

let tryFindUser id =
    users |> List.tryFind (fun (i, _) -> i = id) |> Option.map snd

for id in [1; 3] do
    match tryFindUser id with
    | Some name -> printfn "Hello, %s" name
    | None -> printfn "Unknown user"
Hello, Ada
Unknown user