I think the succinct summary of why null is undesirable is that meaningless states should not be representable.

Suppose I'm modeling a door. It can be in one of three states: open, shut but unlocked, and shut and locked. Now I could model it along the lines of

class Door private bool isShut private bool isLocked

and it is clear how to map my three states into these two boolean variables. But this leaves a fourth, undesired state available: isShut==false && isLocked==true . Because the types I have selected as my representation admit this state, I must expend mental effort to ensure that the class never gets into this state (perhaps by explicitly coding an invariant). In contrast, if I were using a language with algebraic data types or checked enumerations that lets me define

type DoorState = | Open | ShutAndUnlocked | ShutAndLocked

then I could define

class Door private DoorState state

and there are no more worries. The type system will ensure that there are only three possible states for an instance of class Door to be in. This is what type systems are good at - explicitly ruling out a whole class of errors at compile-time.

The problem with null is that every reference type gets this extra state in its space that is typically undesired. A string variable could be any sequence of characters, or it could be this crazy extra null value that doesn't map into my problem domain. A Triangle object has three Point s, which themselves have X and Y values, but unfortunately the Point s or the Triangle itself might be this crazy null value that is meaningless to the graphing domain I'm working in. Etc.

When you do intend to model a possibly-non-existent value, then you should opt into it explicitly. If the way I intend to model people is that every Person has a FirstName and a LastName , but only some people have MiddleName s, then I would like to say something like

class Person private string FirstName private Option<string> MiddleName private string LastName

where string here is assumed to be a non-nullable type. Then there are no tricky invariants to establish and no unexpected NullReferenceException s when trying to compute the length of someone's name. The type system ensures that any code dealing with the MiddleName accounts for the possibility of it being None , whereas any code dealing with the FirstName can safely assume there is a value there.

So for example, using the type above, we could author this silly function:

let TotalNumCharsInPersonsName(p:Person) = let middleLen = match p.MiddleName with | None -> 0 | Some(s) -> s.Length p.FirstName.Length + middleLen + p.LastName.Length

with no worries. In contrast, in a language with nullable references for types like string, then assuming

class Person private string FirstName private string MiddleName private string LastName

you end up authoring stuff like

let TotalNumCharsInPersonsName(p:Person) = p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

which blows up if the incoming Person object does not have the invariant of everything being non-null, or

let TotalNumCharsInPersonsName(p:Person) = (if p.FirstName=null then 0 else p.FirstName.Length) + (if p.MiddleName=null then 0 else p.MiddleName.Length) + (if p.LastName=null then 0 else p.LastName.Length)

or maybe

let TotalNumCharsInPersonsName(p:Person) = p.FirstName.Length + (if p.MiddleName=null then 0 else p.MiddleName.Length) + p.LastName.Length

assuming that p ensures first/last are there but middle can be null, or maybe you do checks that throw different types of exceptions, or who knows what. All these crazy implementation choices and things to think about crop up because there's this stupid representable-value that you don't want or need.

Null typically adds needless complexity. Complexity is the enemy of all software, and you should strive to reduce complexity whenever reasonable.