8.8 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 three 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.

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)

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)
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: `haxevar 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: haxevar nullable:Null<String> = getSomeStr();//var s:String = nullable; // Compilation errorif (nullable != null) { s = nullable; //OK}//s = nullable; // Compilation errors = (nullable == null ? 'hello' : nullable); // OKswitch (nullable) { case null: case _: s = nullable; // OK}* Control flow is also taken into account: haxefunction 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<>. `haxevar a:Array<String> = ["hello"];$type(a[100]); // Stringtrace(a[100]); // nullvar 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.haxevar a:Array<String> = ["hello"];a[2] = "world";trace(a); // ["hello", null, "world"]var s:String = a[1]; // Cannot check thistrace(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: `haxeusing 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: haxevar a:Null<String> = getSomeStr();var fn = function () { if (a != null) { var s:String = a; // Compilation error }}Unless the closure is executed immediately:haxevar 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: haxevar nullable:Null<String> = getSomeNullableStr();var str:String;if (nullable != null) { str = nullable; // OK doStuff(function () nullable = getSomeNullableStr()); if (nullable != null) { str = nullable; // Compilation error }}`