I want to know what are the main differences between Rust's Option::None
and "null" in other programming languages. And why is None
considered to be better?

- 19,194
- 5
- 54
- 65

- 71
- 5
-
3The question assumes `null` means the same thing in other languages. It does not. And a lot of them don't call it `null`: Ruby's `nil`, Perl's `undef`. Could you give some examples of other programming language's null? – Schwern Sep 10 '22 at 16:57
2 Answers
A brief history lesson in how to not have a value!
In the beginning, there was C. And in C, we had these things called pointers. Oftentimes, it was useful for a pointer to be initialized later, or to be optional, so the C community decided that there would be a special place in memory, usually memory address zero, which we would all just agree meant "there's no value here". Functions which returned T*
could return NULL
(written in all-caps in C, since it's a macro) to indicate failure, or lack of value. In a low-level language like C, in that day and age, this worked fairly well. We were only just rising out of the primordial ooze that is assembly language and into the realm of typed (and comparatively safe) languages.
C++ and later Java basically aped this approach. In C++, every pointer could be NULL
(later, nullptr
was added, which is not a macro and is a slightly more type-safe NULL
). In Java, the problem becomes more clear: Every non-primitive value (which means, basically, every value that isn't a number or character) could be null
. If my function takes a String
as argument, then I always have to be ready to handle the case where some naive young programmer passes me a null
. If I get a String
as a result from a function, then I have to check the docs to see if it might not actually exist.
That gave us NullPointerException
, which even today crowds this very website with tens of questions every day by young programmers falling into traps.
It is clear, in the modern day, that "every value might be null
" is not a sustainable approach in a statically typed language. (Dynamically typed languages tend to be more prepared to deal with failure at every point anyway, by their very nature, so the existence of, say, nil
in Ruby or None
in Python is somewhat less egregious.)
Kotlin, which is often lauded as a "better Java", approaches this problem by introducing nullable types. Now, not every type is nullable. If I have a String
, then I actually have a String
. If I intend that my String
be nullable, then I can opt-in to nullability with String?
, which is a type that's either a string or null
. Crucially, this is type-safe. If I have a value of type String?
, then I can't call String
methods on it until I do a null
check or make an assertion. So if x
has type String?
, then I can't do x.toLowerCase()
unless I first do one of the following.
- Put it inside a
if (x != null)
, to make sure thatx
is notnull
(or some other form of control flow that proves my case). - Use the
?
null-safe call operator to dox?.toLowerCase()
. This will compile to anif (x != null)
check and will return aString?
which isnull
if the original string wasnull
. - Use the
!!
to assert that the value is notnull
. The assertion is checked and will throw an exception if I turn out to be wrong.
Note that (3) is what Java does by default at every turn. The difference is that, in Kotlin, the "I'm asserting that I know better than the type checker" case is opt-in, and you have to go out of your way to get into the situation where you can get a null pointer exception. (I'm glossing over platform types, which are a convenient hack in the type system to interoperate with Java. It's not really germane here)
Nullable types is how Kotlin solves the problem, and it's how Typescript (with --strict
mode) and Scala 3 (with null checks turned on) handle the problem as well. However, it's not really viable in Rust without significant changes to the compiler. That's because nullable types require the language to support subtyping. In a language like Java or Kotlin, which is built using object-oriented principles first, it's easy to introduce new subtypes. String
is a subtype of String?
, and also of Any
(essentially java.lang.Object
), and also of Any?
(any value and also null
). So a string like "A"
has a principal type of String
but it also has all of those other types, by subtyping.
Rust doesn't really have this concept. Rust has a bit of type coercion available with trait objects, but that's not really subtyping. It's not correct to say that String
is a subtype of dyn Display
, only that a String
(in an unsized context) can be coerced automatically into a dyn Display
.
So we need to go back to the drawing board. Nullable types don't really work here. However, fortunately, there's another way to handle the problem of "this value may or may not exist", and it's called optional types. I wouldn't hazard a guess as to what language first tried this idea, but it was definitely popularized by Haskell and is common in more functional languages.
In functional languages, we often have a notion of principal types, similar to in OOP languages. That is, a value x
has a "best" type T
. In OOP languages with subtyping, x
might have other types which are supertypes of T
. However, in functional languages without subtyping, x
truly only has one type. There are other types that can unify with T
, such as (written in Haskell's notation) forall a. a
. But it's not really correct to say that the type of x
is forall a. a
.
The whole nullable type trick in Kotlin relied on the fact that "abc"
was both a String
and a String?
, while null
was only a String?
. Since we don't have subtyping, we'll need two separate values for the String
and String?
case.
If we have a structure like this in Rust
struct Foo(i32);
Then Foo(0)
is a value of type Foo
. Period. End of story. It's not a Foo?
, or an optional Foo
, or anything like that. It has one type.
However, there's this related value called Some(Foo(0))
which is an Option<Foo>
. Note that Foo(0)
and Some(Foo(0))
are not the same value, they just happen to be related in a fairly natural way. The difference is that while a Foo
must exist, an Option<Foo>
could be Some(Foo(0))
or it could be a None
, which is kind of like our null
in Kotlin. We still have to check whether or not the value is None
before we can do anything. In Rust, we generally do that with pattern matching, or by using one of the several built-in functions that do the pattern matching for us. It's the same idea, just implementing using techniques from functional programming.
So if we want to get the value out of an Option
, or a default if it doesn't exist, we can write
my_option.unwrap_or(0)
If we want to do something to the inside if it exists, or null
out if it doesn't, then we write
my_option.and_then(|inner_value| ...)
This is basically what ?.
does in Kotlin. If we want to assert that a value is present and panic otherwise, we can write
my_option.unwrap()
Finally, our general purpose Swiss army knife for dealing with this is pattern matching.
match my_option {
None => {
...
}
Some(value) => {
...
}
}
So we have two different approaches to dealing with this problem: nullable types based on subtyping, and optional types based on composition. "Which is better" is getting into opinion a bit, but I'll try to summarize the arguments I see on both sides.
Advocates of nullable types tend to focus on ergonomics. It's very convenient to be able to do a quick "null check" and then use the same value, rather than having to jump through hoops of unwrapping and wrapping values constantly. It's also nice being able to pass literal values to functions expecting String?
or Int?
without worrying about bundling them or constantly checking whether they're in a Some
or not.
On the other hand, optional types have the benefit of being less "magic". If nullable types didn't exist in Kotlin, we'd be somewhat out of luck. But if Option
didn't exist in Rust, someone could write it themselves in a few lines of code. It's not special syntax, it's just a type that exists and has some (ordinary) functions defined on it. It's also built up of composition, which means (naturally) that it composes better. That is, (T?)?
is equivalent to T?
(the former still only has one null
value), while Option<Option<T>>
is distinct from Option<T>
. I don't recommend writing functions that return Option<Option<T>>
directly, but you can end up getting bitten by this when writing generic functions (i.e. your function returns S?
and the caller happens to instantiate S
with Int?
).
I go a bit more into the differences in this post, where someone asked basically the same question but in Elm rather than Rust.

- 62,821
- 6
- 74
- 116
-
1Quick note- Rust's `Option` does have a little magic applied, mainly with the still unstable try trait, as well as being a lang item so it can be used to help out with things like for loop desugaring. That being said, mimicking most of what's done on stable is still entirely possible. – Aiden4 Sep 10 '22 at 18:43
There's two implicit assumptions in the question: first, that other language's "nulls" (and nil
s and undef
s and none
s) are all the same. They aren't.
The second assumption is that "null" and "none" provide similar functionality. There's many different uses of null: the value is unknown (SQL trinary logic), the value is a sentinel (C's null pointer and null byte), there was an error (Perl's undef
), and to indicate no value (Rust's Option::none
).
Null itself comes in at least three different flavors: an invalid value, a keyword, and special objects or types.
Keyword as null
Many languages opt for a specific keyword to indicate null. Perl has undef
, Go and Ruby have nil
, Python has None
. These are useful in that they are distinct from false or 0.
Invalid value as null
Unlike having a keyword, these are things within the language which mean a specific value, but are used as null anyway. The best examples are C's null pointer and null bytes.
Special objects and types as null
Increasingly, languages will use special objects and types for null. These have all the advantages of a keyword, but rather than a generic "something went wrong" they can have very specific meaning per domain. They can also be user-defined offering flexibility beyond what the language designer intended. And, if you use objects, they can have additional information attached.
For example, std::ptr::null
in Rust indicates a null raw pointer. Go has the error
interface. Rust has Result::Err
and Option::None
.
Null as unknown
Most programming languages use two-value or binary logic: true or false. But some, particularly SQL, use three-value or trinary logic: true, false, and unknown. Here null means "we do not know what the value is". It's not true, it's not false, it's not an error, it's not empty, it's not zero: the value is unknown. This changes how the logic works.
If you compare unknown to anything, even itself, the result is unknown. This allows you to draw logical conclusions based on incomplete data. For example, Alice is 7'4" tall, we don't know how tall Bob is. Is Alice taller than Bob? Unknown.
Null as uninitialized
When you declare a variable, it has to contain some value. Even in C, which famously does not initialize variables for you, it contains whatever value happened to be sitting in memory. Modern languages will initialize values for you, but they have to initialize it to something. Often that something is null.
Null as sentinel
When you have a list of things and you need to know when the list is done, you can use a sentinel value. This is anything which is not a valid value for the list. For example, if you have a list of positive numbers you can use -1.
The classic example is C. In C, you don't know how long an array is. You need something to tell you to stop. You can pass around the size of the array, or you can use a sentinel value. You read the array until you see the sentinel. A string in C is just an array of characters which ends with a "null byte", that's just a 0 which is an invalid character. If you have an array of pointers, you can end it with a null pointer. The disadvantages are 1) there's not always a truly invalid value and 2) if you forget the sentinel you walk off the end of the array and bad stuff happens.
A more modern example is how to know to stop iterating. For example in Go, for thing := range things { ... }
, range
will return nil
when there are no more items in the range causing the for
loop to exit. While this is more flexible, it has the same problem as the classic sentinel: you need a value which will never appear in the list. What if null is a valid value?
Languages such as Python and Ruby choose to solve this problem by raising a special exception when the iteration is done. Both will raise StopIteration
which their loops will catch and then exit the loop. This avoids the problem of choosing a sentinel value, Python and Ruby iterators can return anything.
Null as error
While many languages use exceptions for error handling, some languages do not. Instead, they return a special value to indicate an error, often null. C, Go, Perl, and Rust are good examples.
This has the same problem as a sentinel, you need to use a value which is not a valid return value. Sometimes this is not possible. For example, functions in C can only return a single value of a given type. If it returns a pointer, it can return a null pointer to indicate error. But if it returns a number, you have to pick an otherwise valid number as the error value. This conflating the error and return values is a problem.
Go works around this by allowing functions to return multiple values, typically the return value and an error
. Then the two values can be checked independently. Rust can only return a single value, so it works around this by returning the special type Result
. This contains either an Ok
with the return value or an Err
with an error code.
In both Rust and Go, they are not just values, but they can have methods called on them expanding their functionality. Rust's Result::Err
has the additional advantage of being a special type, you can't accidentally use a Result::Err
as anything else.
Null as no value
Finally, we have "none of the given options". Quite simply, there's a set of valid options and the result is none of them. This is distinct from "unknown" in that we know the value is not in the valid set of values. For example, if I asked "which fruit is this car" the result would be null meaning "the car is not any fruit".
When asking for the value of a key in a collection, and that key does not exist, you will get "no value". For example, Rust's HashMap get
will return None
if the key does not exist.
This is not an error, though they often get confused. For example, Ruby will raise ArgumentError
if you pass nonsense into a function. For example, array.first(-2)
asks for the first -2 values which is nonsense and will raise an ArgumentError
.
Option vs Result
Which finally brings us back to Option::None
. It is the "special type" version of null which has many advantages.
Rust uses Option
for many things: to indicate an uninitialized value, to indicate simple errors, as no value, and for Rust specific things. The documentation gives numerous examples.
- Initial values
- Return values for functions that are not defined over their entire input range (partial functions)
- Return value for otherwise reporting simple errors, where None is returned on error
- Optional struct fields
- Struct fields that can be loaned or “taken”
- Optional function arguments
- Nullable pointers
- Swapping things out of difficult situations
To use it in so many places dilutes its value as a special type. It also overlaps with Result
which is what you're supposed to use to return results from functions.
The best advice I've seen is to use Result<Option<T>, E>
to separate the three possibilities: a valid result, no result, and an error. Jeremy Fitzhardinge gives a good example of what to return from querying a key/value store.
...I'm a big advocate of returning
Result<Option<T>, E>
whereOk(Some(value))
means "here's the thing you asked for",Ok(None)
means "the thing you asked for definitely doesn't exist", andErr(...)
means "I can't say whether the thing you asked for exists or not because something went wrong".

- 153,029
- 25
- 195
- 336