jonnifyfsharp

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

C2.03.1 null → Option C2.03.2 exceptions → Result C2.03.3 the interop boundary absence failure the seam
C2.03.1 · From null to Option

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.

You learn how F# represents 'missing' without null

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.

live module hub built dive ready soon specified

References

Sources

Related in this course