| title | description |
|---|---|
Null safety: Frequently asked questions |
Common questions and practical answers to help you migrate your Dart code to null safety. |
Note
This document is archived. It was originally published and maintained on dart.dev between 2020 and 2025. It might contain outdated information.
If you're not migrating a pre-null-safety codebase, check out the current Dart language documentation and type system introduction instead.
This page collects some common questions about null safety with answers based on the experience of migrating Google internal code and packages maintained by the Dart team.
Most of the effects of migration don't immediately affect users of migrated code:
- Static null safety checks for users first apply when they migrate their code.
- Full null safety checks happen when all the code is migrated and sound mode is turned on.
Two exceptions to be aware of are:
- The
!operator is a runtime null check in all modes, for all users. So, when migrating, ensure that you only add!where it's an error for anullto flow to that location, even if the calling code has not migrated yet. - Runtime checks associated with the
latekeyword apply in all modes, for all users. Only mark a fieldlateif you are sure it is always initialized before it is used.
If a value is only ever null in tests, the code can be improved by
marking it non-nullable and making the tests pass non-null values.
The @required annotation marks named arguments that must be passed; if not,
the analyzer reports a hint.
With null safety, a named argument with a non-nullable type must either
have a default or be marked with the new required keyword.
Otherwise, it wouldn't make sense for it to be non-nullable, because it
would default to null when not passed.
When null-safe code is called from legacy code,
the required keyword is treated exactly like the @required annotation:
failure to supply the argument will cause an analyzer hint.
When null-safe code is called from null-safe code, failing to
supply a required argument is an error.
What does this mean for migration?
Be careful if adding required where there was no @required before.
Any callers not passing the newly-required argument will no longer compile.
Instead, you could add a default or make the argument type nullable.
Some computations can be moved to the static initializer. Instead of:
// Initialized without values:
ListQueue _context;
Float32List _buffer;
dynamic _readObject;
Vec2D(Map<String, dynamic> object) {
_buffer = Float32List.fromList([0.0, 0.0]);
_readObject = object['container'];
_context = ListQueue<dynamic>();
}You can do:
// Initialized with values:
final ListQueue _context = ListQueue<dynamic>();
final Float32List _buffer = Float32List.fromList([0.0, 0.0]);
final dynamic _readObject;
Vec2D(Map<String, dynamic> object) : _readObject = object['container'];However, if a field is initialized by doing computation in the constructor,
then it can't be final. With null safety,
you'll find this also makes it harder for it to be non-nullable;
if it's initialized too late, then it's null until
it's initialized, and must be nullable.
Fortunately, you have options:
- Turn the constructor into a factory, then make it delegate to an actual
constructor that initializes all the fields directly.
A common name for such a private constructor is just an underscore:
_. Then, the field can befinaland non-nullable. This refactoring can be done before the migration to null safety. - Or, mark the field
late final. This enforces that it's initialized exactly once. It must be initialized before it can be read.
Getters that were annotated @nullable should instead have nullable types;
then remove all @nullable annotations. For example:
@nullable
int get count;Becomes:
int? get count;Getters that were not marked @nullable should not have nullable types,
even if the migration tool suggests them.
Add ! hints as needed, then rerun the analysis.
Prefer factories that do not return null. We have seen code that meant to throw an exception due to invalid input but instead ended up returning null.
Instead of:
factory StreamReader(dynamic data) {
StreamReader reader;
if (data is ByteData) {
reader = BlockReader(data);
} else if (data is Map) {
reader = JSONBlockReader(data);
}
return reader;
}Do:
factory StreamReader(dynamic data) {
if (data is ByteData) {
// Move the readIndex forward for the binary reader.
return BlockReader(data);
} else if (data is Map) {
return JSONBlockReader(data);
} else {
throw ArgumentError('Unexpected type for data.');
}
}If the intent of the factory was indeed to return null, then you can
turn it into a static method so it is allowed to return null.
The assert will be unnecessary when everything is fully migrated, but for now it is needed if you actually want to keep the check. Options:
- Decide that the assert is not really necessary, and remove it. This is a change in behavior when asserts are enabled.
- Decide that the assert can be checked always, and
turn it into
ArgumentError.checkNotNull. This is a change in behavior when asserts are not enabled. - Keep the behavior exactly as is: add
// ignore: unnecessary_null_comparisonto bypass the warning.
The compiler flags an explicit runtime null check as an unnecessary
comparison if you make arg non-nullable.
if (arg == null) throw ArgumentError(...)You must include this check if the program is a mixed-version one.
Until everything is fully migrated and the code switches to running
with sound null safety, arg might be set to null.
The simplest way to preserve behavior is change the check into
ArgumentError.checkNotNull.
The same applies to some runtime type checks.
If arg has static type String, then if (arg is! String) is
actually checking whether arg is null.
It might look like migrating to null safety means arg can never be null,
but it could be null in unsound null safety.
So, to preserve behavior, the null check should remain.
Import package:collection and
use the extension method firstWhereOrNull instead of firstWhere.
Unlike the late final suggestion above,
these attributes can't be marked as final.
Often, settable attributes also don't have initial values since
they are expected to be set sometime later.
In such cases, you have two options:
-
Set it to an initial value. Often times, the omission of an initial value is by mistake rather than deliberate.
-
If you are sure that the attribute needs to be set before accessed, mark it as
late.WARNING: The
latekeyword adds a runtime check. If any user callsgetbeforesetthey'll get an error at runtime.
The
lookup operator
on Map ([]) by default returns a nullable type.
There's no way to signal to
the language that the value is guaranteed to be there.
In this case, you should use the not-null assertion operator (!) to
cast the value back to V:
return blockTypes[key]!;Which will throw if the map returns null.
If you want explicit handling for that case:
var result = blockTypes[key];
if (result != null) return result;
// Handle the null case here, e.g. throw with explanation.It is typically a code smell to end up with nullable code like this:
List<Foo?> fooList; // fooList can contain null valuesThis implies fooList might contain null values. This might happen if
you are initializing the list with length and filling it in via a loop.
If you are simply initializing the list with the same value,
you should instead use the
filled constructor.
Before (won't compile with null safety):
_jellyCounts = List<int?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
_jellyCounts[i] = 0; // List initialized with the same value.
}After:
_jellyCounts = List<int>.filled(jellyMax + 1, 0); // List initialized with filled constructor.If you are setting the elements of the list via an index, or you are populating each element of the list with a distinct value, you should instead use the list literal syntax to build the list.
Before (won't compile with null safety):
_jellyPoints = List<Vec2D?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
_jellyPoints[i] = Vec2D(); // Each list element is a distinct Vec2D.
}After:
_jellyPoints = [
for (var i = 0; i <= jellyMax; i++)
Vec2D() // Each list element is a distinct Vec2D.
];To generate a fixed-length list,
use the List.generate constructor
with the growable parameter set to false:
_jellyPoints = List.generate(jellyMax, (_) => Vec2D(), growable: false);You might encounter this error:
The default 'List' constructor isn't available when null safety is enabled. #default_list_constructor
The default list constructor fills the list with null, which is a problem.
Change it to List.filled(length, default) instead.
I'm using package:ffi and get a failure with Dart_CObject_kUnsupported when I migrate. What happened?
Lists sent with FFI can only be List<dynamic>,
not List<Object> or List<Object?>.
If you didn't change a list type explicitly in your migration,
a type might still have changed because of changes to type inference that
happen when you enable null safety.
The fix is to explicitly create such lists as List<dynamic>.
The migration tool adds /* == false */ or /* == true */ comments when it
sees conditions that will always be false or true while running in sound mode.
Comments like these might indicate that the automatic migration is
incorrect and needs human intervention. For example:
if (registry.viewFactory(viewDescriptor.id) == null /* == false */)In these cases, the migration tool can't distinguish defensive-coding situations
and situations where a null value is really expected.
So the tool tells you what it knows
("it looks like this condition will always be false!") and
lets you decide what to do.
Null safety brings many benefits like reduced code size and improved app performance. Such benefits surface more when compiled to native targets like Flutter and AOT. Previous work on the production web compiler had introduced optimizations similar to what null safety later introduced. This might make resulting gains to production web apps seem less than their native targets.
A few notes that are worth highlighting:
-
The production JavaScript compiler generates
!not-null assertions. You might not notice them when comparing the output of the compiler before and after adding not-null assertions. That's because the compiler already generated null checks in programs that weren't null safe. -
The compiler generates these not-null assertions regardless of the soundness of null safety or optimization level. In fact, the compiler doesn't remove
!when using-O3or--omit-implicit-checks. -
The production JavaScript compiler might remove unnecessary null checks. This happens because the optimizations that the production web compiler made prior to null safety removed those checks when it knew the value was not null.
-
By default, the compiler would generate parameter subtype checks. These runtime checks ensure covariant virtual calls have appropriate arguments. The compiler skips these checks with the
--omit-implicit-checksoption. Using this option can generate apps with unexpected behavior if the code includes invalid types. To avoid any surprises, continue to provide strong test coverage for your code. In particular, the compiler optimizes code based on the fact that inputs should comply with the type declaration. If the code provides arguments of an invalid type, those optimizations would be wrong and the program could misbehave. This was true for inconsistent types before, and is true with inconsistent nullabilities now with sound null-safety. -
You might notice that the development JavaScript compiler and the Dart VM have special error messages for null checks, but to keep applications small, the production JavaScript compiler does not.
-
You might see errors indicating that
.toStringis not found onnull. This is not a bug. The compiler has always encoded some null checks in this way. That is, the compiler represents some null checks compactly by making an unguarded access of a property of the receiver. So instead ofif (a == null) throw, it generatesa.toString. ThetoStringmethod is defined in JavaScript Object and is a fast way to verify that an object is not null.If the very first action after a null check is an action that crashes when the value is null, the compiler can remove the null check and let the action cause the error.
For example, a Dart expression
print(a!.foo());could turn directly into:P.print(a.foo$0());
This is because the call
a.foo$()will crash ifais null. If the compiler inlinesfoo, it will preserve the null check. So for example, iffoowasint foo() => 1;, the compiler might generate:a.toString; P.print(1);
If the inlined method first accessed a field on the receiver, like
int foo() => this.x + 1;, then the production compiler can remove the redundanta.toStringnull check, as non-inlined calls, and generate:P.print(a.x + 1);