8.9 Null Safety

since Haxe 4.0.0

The Haxe compiler offers opt-in compile-time checking for nullable values. It attempts to catch various possible issues with nullable values.

Enabling Null Safety

To enable the checker for a particular class, field, or expression, annotate it with the :nullSafety metadata. Null safety can be enabled for a whole package using the --macro nullSafety("some.package") initialization macro.

Strictness

There are four levels of null safety strictness:

  • Off: Turn off null safety checks. Useful to selectively disable null safety for particular fields or expression.
  • Loose (Default): Within an if (<expr> != null) condition, <expr> is considered safe even if it could be modified after the check.
  • Strict: Full-scale null safety checking for a single-threaded environment.
  • StrictThreaded: Full-scale null safety checking for a multi-threaded environment.

Enabling null safety by default uses the loose strictness level. This can be configured by providing an argument in the metadata:

@:nullSafety(Off)
@:nullSafety(Loose)
@:nullSafety(Strict)
@:nullSafety(StrictThreaded)

Strict and StrictThreaded differ in handling of sequential field access. In a multi-threaded application sequential access to the same object field may not yeld the same result. That means a null check for a field does not provide any guarantees:

@:nullSafety(StrictThreaded)
function demo1(o:{field:Null<String>}) {
  if (o.field != null) {
    // Error: o.field could have been changed to `null`
    // by another thread after the check
    trace(o.field.length);
  }
}

@:nullSafety(Strict)
function demo1(o:{field:Null<String>}) {
  if (o.field != null) {
    trace(o.field.length); // Ok
  }
}

For the package-level case, null safety strictness can be configured using the optional second argument:

--macro nullSafety("some.package", Off)
--macro nullSafety("some.package", Loose)
--macro nullSafety("some.package", Strict)
--macro nullSafety("some.package", StrictThreaded)
Detailed Usage
  • Null safety makes sure you will not pass nullable values to the places which are not explicitly declared with Null<T> (assignments, return statements, array access, etc.).
@:nullSafety
class Main {
  static function getNullableStr():Null<String> {
    return null;
  }

  public static function main() {
    function fn(s:String) {}
    var nullable:Null<String> = getNullableStr();
    // all of the following lines would cause a compilation error:
    // var str:String = null;
    // var str:String = nullable;
    // fn(nullable);
  }
}
  • Using nullables with unary and binary operators (except == and !=) is not allowed.
  • If a field is declared without Null<> then it should have an initial value or it should be initialized in the constructor (for instance fields).
  • Passing an instance of a parametrized type with nullable type parameters where the same type with non-nullable type parameters is expected is not allowed:
var nullables:Array<Null<String>> = ['hello', null, 'world'];
// Array<Null<String>> cannot be assigned to Array<String>:
//var a:Array<String> = nullables;
  • Local variables checked against null are considered safe inside of a scope covered with that null-check:
var nullable:Null<String> = getSomeStr();
//var s:String = nullable; // Compilation error
if (nullable != null) {
  s = nullable; //OK
}
//s = nullable; // Compilation error
s = (nullable == null ? 'hello' : nullable); // OK
switch (nullable) {
  case null:
  case _: s = nullable; // OK
}
  • Control flow is also taken into account:
function doStuff(a:Null<String>) {
  if(a == null) {
    return;
  }
  // From here `a` is safe, because function execution
  // will continue only if `a` is not null:
  var s:String = a; // OK
}
Limitations
  • Out-of-bounds array reads return null, but Haxe types them without Null<>.
var a:Array<String> = ["hello"];
$type(a[100]); // String
trace(a[100]); // null
var s:String = a[100]; // Safety does not complain here, because `a[100]` is not `Null<String>`, but just `String`
  • Out-of-bounds array writes fill all positions between the last defined index and the newly-written one with null values. Null safety cannot protect against this.
var a:Array<String> = ["hello"];
a[2] = "world";
trace(a); // ["hello", null, "world"]
var s:String = a[1]; // Cannot check this
trace(s); //null
  • Haxe was not designed with null safety in mind, so it's always possible null values will come into your code from third-party code or even from the standard library.
  • Nullable fields and properties are not considered null-safe even after checking against null. You can use helper methods instead:
using Main.NullTools;
class NullTools {
  public static function sure<T>(value:Null<T>):T {
    if (value == null) {
      throw "null pointer in .sure() call";
    }
    return @:nullSafety(Off) (value:T);
  }
  public static function or<T>(value:Null<T>, defaultValue:T):T {
    if (value == null) {
      return defaultValue;
    }
    return @:nullSafety(Off) (value:T);
  }
}
class Main {
  static var nullable:Null<String>;
  public static function main() {
    var str:String;
    if (nullable != null) {
      str = nullable; // Compilation error
    }
    str = nullable.sure();
    str = nullable.or('hello');
  }
}
  • If a local variable is captured in a closure, it cannot be safe inside that closure:
var a:Null<String> = getSomeStr();
var fn = function () {
  if (a != null) {
    var s:String = a; // Compilation error
  }
}

Unless the closure is executed immediately:

var a:Null<String> = getSomeStr();
[1, 2, 3].map(function (i) {
  if (a != null) {
    return i * a.length; // OK
  } else {
    return i;
  }
});
  • If a local variable is captured and modified in a closure with a nullable value, that variable cannot be safe anymore:
var nullable:Null<String> = getSomeNullableStr();
var str:String;
if (nullable != null) {
  str = nullable; // OK
  doStuff(function () nullable = getSomeNullableStr());
  if (nullable != null) {
    str = nullable; // Compilation error
  }
}