Dart null safety - Solving the billion-dollar mistake

In a 2009 talk, Tony Hoare traced the invention of the null pointer to his design of the Algol W language and called it a “mistake”:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Source: https://en.wikipedia.org/wiki/Void_safety

What is null safety?

Null was devised as a special value to represent the intentional absence of any value. This is different from an empty string, a zero int or false. It may or may not exist.

Color favoriteColor;

favoriteColor can be either a Color object or null. This distinction is important understand. Developers may incorrectly refer to the null value as:

Color object which has a null value

On an average, 80 - 90% of variables in our code will have a value. An int will be initialized to a number or is set before being used. This is valid and no harm will fall upon your app. But as we all know, this happens more than 10% of the time:

/images/null-safety-npe-everywhere.jpeg#center

To be clear, null-safety does not mean nulls are bad. On the contrary, a null value has valid use cases. For example,

class Person {
  String firstName;
  String middleName;
  String lastName;
}

In the above case, an application can assume firstName and lastName will be present and may perform no checks. middleName may not be present, in which case, it will either be a String or null.

Null safety is crucial to avoid human errors. Null safety provides a declarative mechanism to understand application logic.

Let’s contrast a code block with and without null safety.

Without null safety, a typical code block would look like:

void middleAllCaps(Person p) {
  return p.middleName.toUpperCase();
}

It would work in most cases, and when middleName is null we would encounter the dreaded Null error:

Uncaught TypeError: Cannot read property 'toUpperCase$0' of nullError: 
TypeError: Cannot read property 'toUpperCase$0' of null

Show the above to a developer, and you would get a staunch reply, “No worries! I got this”, and a null check would appear.

void middleAllCaps(Person p) {
  if (p.middleName == null) return '';
  return p.middleName.toUpperCase();
}

/images/null-safety-npe.jpeg#center

Again, this is a good answer but as developers we forget to do this before going to production, and that is a problem. Why not let the language help us deal with potential null variables.

With null safety on, and we’ll talk about how to enable it for dart, we have options of indicating “nullability”.

Non nullable

By default, with null safety on, all variables are non nullable. This will be valid for 80-90% of your code.


class Person {
  String middleName;
}

void middleAllCaps(Person p) {
  return p.middleName.toUpperCase();
}

The code above will not compile because we have indicated middleName as non-nullable but not initialized it.

Error: Field 'middleName' should be initialized because its type 'String' doesn't allow null.
  String middleName;
           ^^^^^^^^^^
Error: Compilation failed.

One solution is to add the constructor with required parameter. The other is late keyword which we will talk about later.

class Person {
    String middleName;
    Person(this.middleName);
}

String middleAllCaps(Person p) {
    return p.middleName.toUpperCase();
}

In essence, Dart will perform flow analysis to ensure a non-nullable variable is initialized.

Nullable or the ?

You can indicate to Dart that a variable is nullable i.e. it can have a value or not. Nullable variables do have the potential for blowing up. Dart will use flow analysis to prove that no such error can occur. If it cannot it will ask the developer to handle it during compilation itself.

class Person {
  String? middleName;
}

void middleAllCaps(Person p) {
  return p.middleName.toUpperCase();
}

This would cause a compilation error.

Error: Method 'toUpperCase' cannot be called on 'String?' because it is potentially null.
  return p.middleName.toUpperCase();
                        ^^^^^^^^^^^
Error: Compilation failed.

Why? The ? indicates that the variable can have a null value. Dart’s flow analysis will identify a potential null error and halt compilation. The developer is expected to explicitly handle the condition such that p.middleName is not accessed or read when the value is null. Or in other words, add a null check.

void middleAllCaps(Person p) {
  return p.middleName?.toUpperCase() ?? '';
}

So far so good! Dart’s flow analysis will detect the null check and continue compilation.

Nullable force override or the !

Dart’s flow analysis will not detect setters especially where classes are involved. What if you have a class with no constructor but you ensure initialization outside of it. Dart may not understand that flow in which case you want to force Dart to assume the value will always be available. You do that by adding the ! to the accessor.

class Person {
  String? middleName;
}

String middleAllCaps(Person p) {
  // The trailing ! symbol will skip null check
  return p.middleName!.toUpperCase();
}

void main() {
  Person p = Person();
  p.middleName = 'Fancy';
  print(middleAllCaps(p));
}

The developer is responsible for setting the value before accessing it. There may be a handful of cases where overriding null check is acceptable but generally considered a code smell.

required keyword

When creating classes using positional non-nullable fields, Dart’s flow analysis may not detect setting of variables. E.g.

class Person {
  String middleName;
  Person({this.middleName});
}

This will yield a compilation error. middleName is non-nullable but the constructor will set it to null if value is not passed. This is not acceptable. You can use the required keyword to indicate that middleName is a required non-nullable positional parameter.

class Person {
  String middleName;
  Person({required this.middleName});
}

late keyword

Not all non-nullable fields may be initialized in the constructor. The analyzer cannot prove that the value will be set before access. Such as the following case:

class Person {
  String middleName;

  void assignRandomMiddleName() {
    middleName = 'Wilcox';
  }
}

String middleAllCaps(Person p) {
  return p.middleName.toUpperCase();
}

void main() {
  Person p = Person();
  p.assignRandomMiddleName();
  print(middleAllCaps(p));
}

In such cases, you should not force override using ! but rather tell Dart that the variable will be initialized before reading the value like so:

class Person {
  late String middleName;
}

If you set the late keyword and do not initialize it, you won’t get a nullError but rather a LateInitializationError:

Uncaught Error: LateInitializationError: Field 'middleName' has not been initialized.

How to enable null safety

Null safety is still experimental. Library developers have already started pushing out null safe versions but you can continue run your apps without switching over.To opt-in to null safety for your project make sure:

  • You have dart version 2.10+
  • All dependencies are null safe

To run an app using null safety on:

dart --enable-experiment=non-nullable --no-sound-null-safety bin/myapp.dart

To enable your IDE to provide null safety errors you have to modify your analysis_options.yaml and add the following options:

# Defines a default set of lint rules enforced for
# projects at Google. For details and rationale,
# see https://github.com/dart-lang/pedantic#enabled-lints.
include: package:pedantic/analysis_options.yaml

# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
# Uncomment to specify additional rules.
# linter:
#   rules:
#     - camel_case_types

analyzer:
  enable-experiment:
      - non-nullable

How do I make my code null safe

dart comes with a migration tool built-in. More options are available on migration in the dart docs

dart migrate