Optional Type Misconceptions
More and more programming languages are being updated to gain a dedicated “optional value” type, rather than relying on null
.
Java gained Optional
in Java 8, C++ gained std::optional
in C++17, and newly-designed languages such as Kotlin or Swift have special syntax for optional values with the ?
operator on a type.
I wholeheartedly welcome this change! I find that it makes my code easier to read, easier to write, and easier to modify.
Why is this so? The problems with null
are well-documented by now, but here’s a summary:
- There’s no way of telling which functions return
null
and which return an actual value, so it’s harder to tell where you need to write error checks (and where you don’t need to). null
values have a habit of propagating themselves far away from where they were initially created. If your program fails because a value is unexpectedlynull
, you have to go back and trace where thisnull
originally came from.
I was lucky enough to have started programming in a language that had no concept of null
from the beginning — Haskell — so I got to experience its benefits from the start.
This reversed the question about null
: I didn’t have to think “should we find some way to solve the null
problem in a future version?”, but instead “should we add null
in a future version?”.
To which the answer was, of course, no.
However, this means that when I use an entrenched lanugage such as Java, which cannot remove null
for the sake of backwards compatibility, I know that the experience of using a type like java.util.Optional
isn’t as good as it could be.
With an enormous amount of existing code, as well as the standard libraries, best practices, and developer habits of working with it, there are cases that using an Optional type makes the language a little more awkward to use.
It helps, but it also hurts.
This post is a list of responses to the most common complaints about the Optional type I’ve seen on the internet, all of which I disagree with.
Misconception #1:Optional doesn’t help because you can still ignore the error
The main benefit of using an Optional type is not just that you can tell which functions can return null
, but that you can tell which functions can’t.
So paradoxically, I find myself writing a lot more sloppy code using unchecked get
or unwrap
calls, purely because it’s much easier to come back and fix it later.
This is best appreciated when doing exploratory design work. For example, here’s some fragile, buggy code I wrote recently, to read values from a database and return them:
final User user = usersTable.findById(1234).get(); // (1) final String name = user.name().formattedName(); final String address = user.getDefaultAddress().get().formattedAddress(); // (2) final String phone = user.getPrimaryPhone(); final PhoneCall lastPhoneCall = phoneCallsTable.findLastCallTo(phone).get(); // (3) final String lastCalledDate = dateFormatter.format(lastPhoneCall.callDate()); return String.format("%s (last phoned at %s)%n%s", name, lastCalledDate, address);
Look at all those calls to .get()
!
This code is very fragile:
(1)
: The user with ID 1234 might not exist in the database.(2)
: They might exist, but have no default address.(3)
: There may be no last phone call associated with their phone number.
Any of these three situations would throw an exception when the get
call fails, crashing our program.
However, when you’re done with the happy path and it’s time to add error handling, these calls to get
will stick out like a sore thumb.
I can see where get
is being used inappropriately, and replace it with better, sturdier code.
Here’s the same code written without using the Optional
type.
All the functions now just return null
, and it now looks like this:
final User user = usersTable.findById(1234); final String name = user.name().formattedName(); final String address = user.getDefaultAddress().formattedAddress(); final String phone = user.getPrimaryPhone(); final PhoneCall lastPhoneCall = phoneCallsTable.findLastCallTo(phone); final String lastCalledDate = dateFormatter.format(lastPhoneCall.callDate()); return String.format("%s (last phoned at %s)%n%s", name, lastCalledDate, address);
Now, which function calls do we need to add null
checks to?
You could probably guess that the two that return entities from the database could return null
, but what about the others? A user might not have a default address, but could they have no primary phone?
Could formattedAddress
fail if their address doesn’t fit the standard format?
Could a phone call have no date?
Without get
explicitly marking places in the code that fail to handle their errors, it becomes a lot harder to deal with missing values where appropriate.
I’m forced to think about both the success path and the error path when I just want to be doing exploratory design work.
The usual way I’ve seen developers solve this problem is to add null
checks everywhere, including the places where it’s not needed.
And now your function is twice as long.
Misconception #2:You now have two failure cases to deal with
Another reason why developers may not trust the Optional type is that in languages where null
is already a thing, such as Java, you now have two failure cases to deal with: an empty Optional<T>
, and null
itself, which is still part of the language.
While this is a problem in theory, it does not turn out to be a problem in practice. There’s a simple rule that cuts one of these failure cases out:
If your Optional value is null
, then you have a bug in your code and your program should crash, 100% of the time.
One of the problems with null
is that you can’t tell the diffence between a null
caused by something being missing, such as a user not being present in the database, and a null
caused by a bug in the program, such as an initialisation function not being run.
Using Optional types will separate these two cases; using straight null
s will not.
Here’s an example to demonstrate:
static Optional<String> overrideAppName = null; // gets loaded later static void loadGlobalConfig() { final Properties props = new Properties(); props.load(new FileInputStream("application.properties")); overrideAppName = Optional.ofNullable(props.getProperty("app-name")); } void welcomeUser() { if (overrideAppName.isPresent()) { System.out.println("Welcome, user, in " + overrideAppName.get()); } else { System.out.println("Welcome, user"); } }
We want our application to have a configurable name, and the name gets loaded from a config file in loadGlobalConfig
.
Then, later, we greet our user in one of two different ways, depending on whether the application name has been set.
However, this whole code is predicated upon the loadGlobalConfig
function actually being called.
If it gets skipped, or if it stops halfway through for whatever reason, then the overrideAppName
will remain null
.
This is a bug! If we changed the code to cater for this case, it would look like this:
if (overrideAppName != null && overrideAppName.isPresent()) { System.out.println("Welcome, user, in " + overrideAppName.get()); } else { System.out.println("Welcome, user"); }
And sure, this particular function will no longer throw an exception. But not only is it highly likely that some other function further down the program will just throw an exception instead, but you’ve covered up a bug.
So an empty Optional and a null
serve two different purposes: one is intentional, and you should handle that case; the other is unintentional, and you should treat as a bug, and crash.
Misconception #3:We don’t need Optional when we already have null
I can’t argue against this statement, as from a technical perspective, it’s entirely correct.
There are no programs that can be written using Optionals that can’t be written using null
:
value.isPresent()
is the same concept asvalue != null
.value.orElse(other)
has the same motive asvalue != null ? other : null
.- Chains of
map
andfilter
methods can be re-written as… well, as code, like we’re used to writing.
The difference with null
is instead a social one.
Java needs a special mention here, because before the Optional
type came along, it tried to solve the problem using @Nullable
and @NotNull
annotations on types.
If your method returned a string that could be null
, then it returned a @Nullable String
, not just a String
. Or if it took a parameter that should never be null
, then it could take a @NotNull Object
, rather than just an Object
. And then you’d have a compile-time processor, such as as the Nullness Checker in the Checker Framework, that would analyse your code and point out anywhere you aren’t checking for null
when you should, or are checking for null
when you don’t need to.
This is essentially the same safety guarantees you get with null
, just done in a different way.
At least it would have been, had this approach taken off; but it hadn’t, which is why Optional
needed to be added to the language instead.
For one thing, it’s opt-in: while your IDE might use these annotations to show warnings, your build server won’t, unless you’ve gone out of your way to set it up so it checks your code as it builds it.
For another, you end up having more non-nullable types than nullable ones, and having to annotate the places that are not null
gets annoying fast.
So while it’s true that you can get away with just using annotations, it’s more error-prone.
And if humans were better at not making errors, we wouldn’t have issues around null
to begin with.
The secret about absence
Those are the three main complaints about Optional types that I’ve seen, and I hope this clears up some of the details around their use. But there’s one more conceptual reason that I favour this type.
Earlier I mentioned languages that have special syntactic support for null
, such as Swift or Kotlin, that were designed after the industry realised the mistakes about sticking with it.
Instead of having to define an Optional string as literally an Optional<String>
, you can just put a ?
after the type, as String?
.
These languages go further and have special syntax for dealing with missing values: the safe navigation operator, ?.
in Kotlin or ??
in Swift, make it easy to cut through null
-heavy procedures without making the code really long.
But languages descended from ML have resisted adding special syntax and operators around missing values. In Haskell or Idris, for example, the Maybe
type is a simple sum type with two possibilities, Just
and Nothing
. And instead of syntax like ??
or ?:
, you have functions like map
or orElse
.
What’s the benefit of avoiding dedicated syntax for this? It’s precisely because it’s dedicated syntax that I don’t like. Having Optional be just another type lets you know its deep, dark secret:
Optional types are not special!
Haskell’s Maybe
is just another data structure, one that you could implement yourself if you wanted.
It’s like a collection, except instead of containing zero or more elements, it contains zero or one element — and has operations like map
or filter
that work in the same way.
I recently worked with some code that had to cache the results of running some functions.
When the functions took a string argument, we had to store one result per string, which was put in a Map
:
Map<String, InvocationResult> invocations;
When the functions did something different the second, third, or fourth time they were called, we stored the results in a List
:
List<InvocationResult> invocations;
And when the functions did the same thing each time, the only thing we needed to know was whether we had cached data for that function or not, so we stored the result in an Optional
:
Optional<InvocationResult> invocation;
It’s being treated like any other container, because that’s what it is: a container of values, just zero or one of them.
These are the reasons I prefer to use Optional types when they are available, instead of falling back to using null
.∎