Module C2.03
From null to Option, from exceptions to Result
Two C# habits to unlearn fastest — and the two types that replace them.
A C# developer reaches for two tools that F# takes away. null stands
for a missing reference; an exception is thrown when an operation cannot proceed. Both are
invisible in a type: a method that returns string might return
null, and a method that returns int might throw — the
signature does not say. F# makes both visible. A value that may be absent has type
Option<'T> — Some v or None; an
operation that may fail with a reason returns Result<'T,'E> —
Ok v or Error e. Each is a two-case value the compiler
forces the caller to handle, so the unhandled-null bug and the forgotten-catch bug stop compiling.
This module covers both in three dives.
Absence and failure, made into values
In C#, the two ways a call can decline to give a normal answer are null and a
thrown exception. Neither shows in the return type, so the compiler cannot remind the caller to handle them —
the reminders arrive at runtime, as a NullReferenceException or an uncaught throw.
F# moves both into the type system. The three dives below take them one at a time, then close on the seam
where F# meets the C# that still does it the old way.
The first dive replaces null with Option:
a value is either Some v or None, and a
match must cover both. The second replaces exceptions-as-control-flow with
Result: an expected failure is Error e
carried in the return type, threaded with Result.map and
Result.bind. The third is the interop boundary — converting the
null and the throw that real C# libraries still hand you into
Option and Result at the edge, so the F# core stays clean.
The three dives · select one
F# types have no null; a value that may be missing has type Option<'T> — Some v or None — and a match must cover both, so the unhandled-null bug stops compiling.
Open this dive · or pick another above
Absence is one type, failure is another, and the compiler reads both off the signature. The caller cannot forget what the call did not return.
The habit, in one line each
The shift is concrete. Where C# returns null for a missing lookup and the caller
guards with ?. and ??, F# returns an
Option and the caller matches. Where C# throws on a bad
parse and the caller wraps a try/catch, F# returns a Result
and the caller reads Ok or Error. The same two examples
run through all three dives.
C# — null for missing, then the guard dance
string? FindName(int id) => _byId.TryGetValue(id, out var n) ? n : null; // caller must remember the value can be null: var shown = FindName(9)?.ToUpper() ?? "unknown"; // "unknown" — but nothing forced the ??
F# — Option for missing, then a match that must be total
let findName id = Map.tryFind id users // val findName : int -> string option findName 9 // val it : string option = None match findName 9 with | Some n -> n.ToUpper() | None -> "unknown" // the compiler requires the None arm
C# habit
null for absence and a thrown exception for failure — neither visible in the return type, neither enforced by the compiler.
F# form
Option for absence, Result for failure — both in the type, both handled because the match must be total.
The three dives
Read them in order. Each stands on valid F# beside the idiomatic C# it replaces: every
// val it : echo below was produced by dotnet fsi, and every
C# line compiles. Together they take a C# developer from null and
try/catch to Option and Result, and
show how to live at the boundary between the two.
From null to Option
F# types have no null; 'missing' is Some v or None, and the match must cover both.
- absencehow F# represents 'missing' without null
From exceptions to Result
Expected failure is Ok v or Error e in the type, threaded with map and bind.
- failurefailure as a returned value, plus the .NET 9 TryParse trap
The interop boundary
Convert the null and the throw at the seam; keep the F# core all Option and Result.
- the seam
Option.ofObj,ofNullable, and a try-wrap
References
Sources
- Microsoft — F# documentation — options, results, and the null-handling guidance for F#.
- FSharp.Core API reference — the
OptionandResultmodules. - Microsoft — .NET documentation —
Nullable<T>, nullable reference types, and exceptions on the CLR.
Related in this course
- C2.03.1 — From null to Option — absence without
null. - C1.08 — Option, Result & railway-oriented errors — the same two types, in language depth.
- Elixir — Purity — the BEAM's
{:ok, v}/{:error, reason}, returned rather than raised.