Taming Null Dereferences with Pluggable Type Systems - Part I

Gitar, IncGitar, Inc
7 min read

This post provides a gentle introduction to both NullAway - an open-source tool to prevent null pointer exceptions in Java - as well as pluggable type systems for nullability in general. It is meant to be the first in a series of posts describing multiple efforts we’ve been involved with over the years to retrofit null safety into popular existing programming languages (i.e. Java and Go). Along the way, we will examine the lessons those efforts provide for developing fast and precise tooling for early detection of entire classes of issues at scale.

Some key takeaways:

  • NPEs are painful, try NullAway to get rid of them! Particularly for any new Java projects (wait for the next blog post on the topic for large existing codebases!)

  • Pluggable type systems in general are a good approach to writing more reliable software and retrofitting checks into existing languages

  • Overhead of static analysis tools matters (a lot!). Reporting errors in local builds >> reporting in CI, that needs blazingly fast tools.

The problem with null

“I call it my billion-dollar mistake. It was the invention of the null reference in 1965.”

-Tony Hoare

Memory-safe and type-safe languages ensure that each reference used in a program points to a valid object or struct of the expected type, with one notable exception: many popular such languages like Java and Go allow the null/nil reference (representing the absence of a value) to be used wherever a reference to any type is expected!

Predictably, this leads to runtime crashes, when programs attempt to perform operations on values that, quite simply, aren’t there:

static void log(Bar  x) {
    System.out.println(x.toString());
}
static void foo() {
    log(null);
}

The code above will compile on a standard Java compiler without issues, yet crash at runtime with a NullPointerException(NPE):

 java.lang.NullPointerException:
    Cannot invoke "Object.toString()" because "<parameter1>" is null

The above message is all too familiar to Java developers: NPEs are one of the most common runtime exceptions in Java, if not the most common.

This pain point is well-known enough that many recent programming languages (e.g. Kotlin, Rust, Swift) do away with general null references altogether, requiring the use of explicit optional types instead for cases where a value may not be present.

That said, plenty of code still uses Java, and even recently developed and popular languages such as Go allow null values (technically nil values in Go, but they are equivalent: a typed zero-addressed pointer). As of 2023, all of the top-10 most popular programming languages hosted in GitHub (by number of PRs) allow for some form of null references!

Pluggable Type Systems to the Rescue

If some languages have solved this problem, a natural thought is: can we transplant their solution to those that haven’t? It turns out that we can! Static analysis tooling can extend and supplement a language’s default type system. We call such user-defined extensions of a programming language’s base type system a pluggable type system.

Restricting ourselves to Java for the rest of this post, we can augment types with annotations, such as @Nullable T or @NonNull T, to distinguish in the source code “a reference to T including possibly the null reference” from “a reference to T excluding null”. Then, similar to how Java by default will refuse to assign a String object to an Integer variable, so too will our tooling block the build if @Nullable T is assigned to @NonNull T. For code we control, we can even specify that T, without the annotation, simply means @NonNull T.

NullAway is an open-source production-quality checker following these principles. It has been deployed at various companies, including Uber, where it checks over 95% of all Java code (the second most used language within the company). The code from the previous section, if built with NullAway, would first report an error on the call to log(null), since a @Nullable Bar is passed to a method expecting an (implicitly) @NonNull Bar.

Adding the corresponding annotation, we have:

static void log(@Nullable Bar  x) {
    System.out.println(x.toString());
}

static void foo() {
    log(null);
}

Now, we can clearly see that we are calling a method (toString()) on a potentially null (that is to say @Nullable) value x. NullAway will report a new error here, requiring an explicit null check before x is dereferenced:

MyClass.java:13: 
    error: [NullAway] dereferenced expression x is @Nullable

We can fix the above with an explicit null check:

static void log(@Nullable Bar  x) {
    System.out.println(x != null ? x.toString() : "No x found");
}

Using NullAway in your codebase can eliminate NPEs in first-party code in practice, and greatly reduce the problems caused by null dereferences in general! Though it does require you to add annotations wherever you intend to pass around null values as fields or method arguments (we perform enough type inference that you don’t need to worry about annotating local variables!).

“The Real World” Considerations

Some brief personal context: My first contact with the idea of extending the Java type system via annotations was when applying to Ph.D. programs, when reading the 2008 seminal paper “Practical Pluggable Types for Java”, which already outlines a nullability type extension for Java. My second was during an internship at Meta (then Facebook), where I learned they had built such a tool (and soon after, that Uber was using it in strict mode for their mobile code, which funnily enough was part of the reason I applied for a job there…).

So, why then, did we1 decide to implement this idea a third time?

The short answer is performance. Tools that report errors to programers are most effective when such errors are reported early in the development process (the management-speak here is “shift-left”).

This is especially important for tools like NullAway, which reports “errors” in coding conventions that don’t always represent a real runtime bug: you need to annotate fields/arguments with @Nullable even when the code handles null properly! This is needed to make the type-checking local and thus tractable (though we will discuss a cool alternative in a future blog post). Asking developers to write their code in a particular way, tracking and annotating nullability of values, is a lot more reasonable when we provide feedback during the edit-build-debug loop, rather than after the fact, when the code is posted for review. For an analogy, consider how burdensome it would be to write Python or JavaScript code locally, only to be asked about strict type annotations exclusively on CI.

NullAway is fast enough to run on every local build, with a time overhead of ~15% over pure javac compilation. At the time we built it, no other alternative was fast enough to run locally, and developers had to wait for CI just to be reminded of a missing annotation:

More recently there are efforts underway to separate the choice of the checker tool from the semantics of the annotations / type system. Namely the JSpecify workgroup, of which NullAway is part.

What’s Next?

This blog post introduced the core concepts around NullAway and pluggable type systems for nullability, but it is far from the whole story. Here are a few things I hope to post about in the near future that couldn’t make it to this Part I (with links to pre-existing info, in case you can’t wait to check stuff out 😉):

  • How to onboard an existing codebase into NullAway. Adding annotations manually is tedious work. We built tooling that can help with that!

  • How we did something similar for Go, and why it wasn’t quite an identical solution.

  • How we deal with third-party Java libraries and other unannotated code called from NullAway enrolled code.

  • How we handle streams, IDLs, API nullability contracts, etc.

  • Examples of pluggable type systems beyond nullness. How about a type system for safe access to the Android UI thread?

We invite you to join our Slack community, where we continue to explore and discuss these topics further.


  1. Manu Sridharan stated the initial prototype of NullAway a few months before I joined. I became a core contributor early on, and for 4-5 years I was NullAway’s main internal maintainer. NullAway has 40+ total contributors.

0
Subscribe to my newsletter

Read articles from Gitar, Inc directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gitar, Inc
Gitar, Inc

At Gitar, we’re building the next generation software development platform – tools that offer a magical developer experience supercharged by advanced code analysis. We enable efficient and reliable development at scale, empowering developers to do the best work of their careers