Fixing type promotion failures
Solutions for cases where you know more about a field's type than Dart can determine.
Type promotion
occurs when flow analysis can soundly confirm that a
variable of type T actually refers to an object
of a subtype S of T. The variable
is then promoted to S. As a special case, the
variable may have a
nullable type, and flow analysis may confirm that it is
not null. The variable is then promoted to the
corresponding non-nullable type. Many circumstances can
weaken a type's soundness, causing type promotion to fail.
This page lists reasons why type promotion failures occur, with tips on how to fix them. To learn more about flow analysis and type promotion, check out the Understanding null safety page.
Unsupported language version for field promotion
#The cause: You're trying to promote a field, but field promotion is language versioned, and your code is set to a language version before 3.2.
If you're already using an SDK version >= Dart 3.2, your code might still be explicitly targeted for an earlier language version. This can happen either because:
-
Your
pubspec.yamldeclares an SDK constraint with a lower bound below 3.2, or -
You have a
// @dart=versioncomment at the top of the file, whereversionis lower than 3.2.
Example:
// @dart=3.1
class C {
final int? _i;
C(this._i);
void f() {
if (_i != null) {
int i = _i; // ERROR
}
}
}
Message:
'_i' refers to a field. It couldn't be promoted because field promotion is only available in Dart 3.2 and above.
Solution:
Ensure your library isn't using a
language version
earlier than 3.2. Check the top of your file for an outdated
// @dart=version comment, or your
pubspec.yaml for an outdated
SDK constraint lower-bound.
Only local variables can be promoted (before Dart 3.2)
#The cause: You're trying to promote a property, but only local variables can be promoted in Dart versions earlier than 3.2, and you are using a version earlier than 3.2.
Example:
class C {
int? i;
void f() {
if (i == null) return;
print(i.isEven); // ERROR
}
}
Message:
'i' refers to a property so it couldn't be promoted.
Solution:
If you are using Dart 3.1 or earlier, upgrade to 3.2 or later.
If you need to keep using an older version, read Other causes and workarounds
Other causes and workarounds
#The remaining examples on this page document reasons for promotion failures unrelated to version inconsistencies, for both field and local variable failures, with examples and workarounds.
In general, the usual fixes for promotion failures are one or more of the following:
- Assign the property's value to a local variable with the non-nullable type you need.
-
Add an explicit null check (for example,
i == null). -
Use
!orasas a redundant check if you're sure an expression can't benull.
Here's an example of creating a local variable (which can be
named i) that holds the value of
i:
class C {
int? i;
void f() {
final i = this.i;
if (i == null) return;
print(i.isEven);
}
}
This example features an instance field, but it could
instead use an instance getter, a static field or getter, a
top-level variable or getter, or
this.
And here's an example of using i!:
print(i!.isEven);
Can't promote this
#
The cause:
You're trying to promote this, but type
promotion for this is not yet supported.
One common this promotion scenario is when
writing
extension methods. If the
on type
of the extension method is a nullable type, you'd want to do
a null check to see whether this is
null:
Example:
extension on int? {
int get valueOrZero {
return this == null ? 0 : this; // ERROR
}
}
Message:
`this` can't be promoted.
Solution:
Create a local variable to hold the value of
this, then perform the null check.
extension on int? {
int get valueOrZero {
final self = this;
return self == null ? 0 : self;
}
}
Only private fields can be promoted
#The cause: You're trying to promote a field, but the field is not private.
It's possible for other libraries in your program to override public fields with a getter. Because getters might not return a stable value, and the compiler can't know what other libraries are doing, non-private fields cannot be promoted.
Example:
class Example {
final int? value;
Example(this.value);
}
void test(Example x) {
if (x.value != null) {
print(x.value + 1); // ERROR
}
}
Message:
'value' refers to a public property so it couldn't be promoted.
Solution:
Making the field private lets the compiler be sure that no outside libraries could possibly override its value, so it's safe to promote.
class Example {
final int? _value;
Example(this._value);
}
void test(Example x) {
if (x._value != null) {
print(x._value + 1);
}
}
Only final fields can be promoted
#The cause: You're trying to promote a field, but the field is not final.
To the compiler, non-final fields could, in principle, be modified any time between the time they're tested and the time they're used. So it's not safe for the compiler to promote a non-final nullable type to a non-nullable type.
Example:
class Example {
int? _mutablePrivateField;
Example(this._mutablePrivateField);
void f() {
if (_mutablePrivateField != null) {
int i = _mutablePrivateField; // ERROR
}
}
}
Message:
'_mutablePrivateField' refers to a non-final field so it couldn't be promoted.
Solution:
Make the field final:
class Example {
final int? _immutablePrivateField;
Example(this._immutablePrivateField);
void f() {
if (_immutablePrivateField != null) {
int i = _immutablePrivateField; // OK
}
}
}
Getters can't be promoted
#The cause: You're trying to promote a getter, but only instance fields can be promoted, not instance getters.
The compiler has no way to guarantee that a getter returns the same result every time. Because their stability can't be confirmed, getters are not safe to promote.
Example:
import 'dart:math';
abstract class Example {
int? get _value => Random().nextBool() ? 123 : null;
}
void f(Example x) {
if (x._value != null) {
print(x._value.isEven); // ERROR
}
}
Message:
'_value' refers to a getter so it couldn't be promoted.
Solution:
Assign the getter to a local variable:
import 'dart:math';
abstract class Example {
int? get _value => Random().nextBool() ? 123 : null;
}
void f(Example x) {
final value = x._value;
if (value != null) {
print(value.isEven); // OK
}
}
External fields can't be promoted
#
The cause:
You're trying to promote a field, but the field is marked
external.
External fields don't promote because they are essentially external getters; their implementation is code from outside of Dart, so there's no guarantee for the compiler that an external field will return the same value each time it's called.
Example:
class Example {
external final int? _externalField;
void f() {
if (_externalField != null) {
print(_externalField.isEven); // ERROR
}
}
}
Message:
'_externalField' refers to an external field so it couldn't be promoted.
Solution:
Assign the external field's value to a local variable:
class Example {
external final int? _externalField;
void f() {
final i = _externalField;
if (i != null) {
print(i.isEven); // OK
}
}
}
Conflict with getter elsewhere in library
#The cause: You're trying to promote a field, but another class in the same library contains a concrete getter with the same name.
Example:
import 'dart:math';
class Example {
final int? _overridden;
Example(this._overridden);
}
class Override implements Example {
@override
int? get _overridden => Random().nextBool() ? 1 : null;
}
void testParity(Example x) {
if (x._overridden != null) {
print(x._overridden.isEven); // ERROR
}
}
Message:
'_overridden' couldn't be promoted because there is a conflicting getter in class 'Override'.
Solution:
If the getter and field are related and need to share their name (like when one of them overrides the other, as in the example above), then you can enable type promotion by assigning the value to a local variable:
import 'dart:math';
class Example {
final int? _overridden;
Example(this._overridden);
}
class Override implements Example {
@override
int? get _overridden => Random().nextBool() ? 1 : null;
}
void testParity(Example x) {
final i = x._overridden;
if (i != null) {
print(i.isEven); // OK
}
}
Note about unrelated classes
#
Note that in the above example it's clear why it's unsafe to
promote the field _overridden: because there's
an override relationship between the field and the getter.
However, a conflicting getter will prevent field promotion
even if the classes are unrelated. For example:
import 'dart:math';
class Example {
final int? _i;
Example(this._i);
}
class Unrelated {
int? get _i => Random().nextBool() ? 1 : null;
}
void f(Example x) {
if (x._i != null) {
int i = x._i; // ERROR
}
}
Another library might contain a class that combines the two
unrelated classes together into the same class hierarchy,
which would cause the reference in function
f to x._i to get dispatched to
Unrelated._i. For example:
class Surprise extends Unrelated implements Example {}
void main() {
f(Surprise());
}
Solution:
If the field and the conflicting entity are truly unrelated, you can work around the problem by giving them different names:
class Example {
final int? _i;
Example(this._i);
}
class Unrelated {
int? get _j => Random().nextBool() ? 1 : null;
}
void f(Example x) {
if (x._i != null) {
int i = x._i; // OK
}
}
Conflict with non-promotable field elsewhere in library
#The cause: You're trying to promote a field, but another class in the same library contains a field with the same name that isn't promotable (for any of the other reasons listed on this page).
Example:
class Example {
final int? _overridden;
Example(this._overridden);
}
class Override implements Example {
@override
int? _overridden;
}
void f(Example x) {
if (x._overridden != null) {
print(x._overridden.isEven); // ERROR
}
}
This example fails because at runtime, x might
actually be an instance of Override, so
promotion would not be sound.
Message:
'overridden' couldn't be promoted because there is a conflicting non-promotable field in class 'Override'.
Solution:
If the fields are actually related and need to share a name, then you can enable type promotion by assigning the value to a final local variable to promote:
class Example {
final int? _overridden;
Example(this._overridden);
}
class Override implements Example {
@override
int? _overridden;
}
void f(Example x) {
final i = x._overridden;
if (i != null) {
print(i.isEven); // OK
}
}
If the fields are unrelated, then rename one of the fields so they don't conflict. Read the Note about unrelated classes.
Conflict with implicit noSuchMethod forwarder
#
The cause:
You're trying to promote a field that is private and final,
but another class in the same library contains an
implicit noSuchMethod forwarder
with the same name as the field.
This is unsound because there's no guarantee that
noSuchMethod
will return a stable value from one invocation to the next.
Example:
import 'package:mockito/mockito.dart';
class Example {
final int? _i;
Example(this._i);
}
class MockExample extends Mock implements Example {}
void f(Example x) {
if (x._i != null) {
int i = x._i; // ERROR
}
}
In this example, _i can't be promoted because
it could resolve to the unsound implicit
noSuchMethod forwarder (also named
_i) that the compiler generates inside
MockExample.
The compiler creates this implicit implementation of
_i because MockExample promises to
support a getter for _i when it implements
Example in its declaration, but doesn't fulfill
that promise. So, the undefined getter implementation is
handled by
Mock's
noSuchMethod definition, which creates an implicit
noSuchMethod forwarder of the same name.
The failure can also occur between fields in unrelated classes.
Message:
'_i' couldn't be promoted because there is a conflicting noSuchMethod forwarder in class 'MockExample'.
Solution:
Define the getter in question so that
noSuchMethod doesn't have to implicitly handle
its implementation:
import 'package:mockito/mockito.dart';
class Example {
final int? _i;
Example(this._i);
}
class MockExample extends Mock implements Example {
@override
late final int? _i;
}
void f(Example x) {
if (x._i != null) {
int i = x._i; // OK
}
}
The getter is declared late to be consistent
with how mocks are generally used; it's not necessary to
declare the getter late to solve this type
promotion failure in scenarios not involving mocks.
Possibly written after promotion
#The cause: You're trying to promote a variable that might have been written to since it was promoted.
Example:
void f(bool b, int? i, int? j) {
if (i == null) return;
if (b) {
i = j; // (1)
}
if (!b) {
print(i.isEven); // (2) ERROR
}
}
Solution:
In this example, when flow analysis hits (1), it demotes
i from non-nullable int back to
nullable int?. A human can tell that the access
at (2) is safe because there's no code path that includes
both (1) and (2), but flow analysis isn't smart enough to
see that, because it doesn't track correlations between
conditions in separate if statements.
You might fix the problem by combining the two
if statements:
void f(bool b, int? i, int? j) {
if (i == null) return;
if (b) {
i = j;
} else {
print(i.isEven);
}
}
In straight-line control flow cases like these (no loops),
flow analysis takes into account the right-hand side of the
assignment when deciding whether to demote. As a result,
another way to fix this code is to change the type of
j to int.
void f(bool b, int? i, int j) {
if (i == null) return;
if (b) {
i = j;
}
if (!b) {
print(i.isEven);
}
}
Possibly written in a previous loop iteration
#The cause: You're trying to promote something that might have been written to in a previous iteration of a loop, and so the promotion was invalidated.
Example:
void f(Link? p) {
if (p != null) return;
while (true) { // (1)
print(p.value); // (2) ERROR
var next = p.next;
if (next == null) break;
p = next; // (3)
}
}
When flow analysis reaches (1), it looks ahead and sees the
write to p at (3). But because it's looking
ahead, it hasn't yet figured out the type of the right-hand
side of the assignment, so it doesn't know whether it's safe
to retain the promotion. To be safe, it invalidates the
promotion.
Solution:
You can fix this problem by moving the null check to the top of the loop:
void f(Link? p) {
while (p != null) {
print(p.value);
p = p.next;
}
}
This situation can also arise in
switch statements if a case block
has a label, because you can use labeled
switch statements to construct loops:
void f(int i, int? j, int? k) {
if (j == null) return;
switch (i) {
label:
case 0:
print(j.isEven); // ERROR
j = k;
continue label;
}
}
Again, you can fix the problem by moving the null check to the top of the loop:
void f(int i, int? j, int? k) {
switch (i) {
label:
case 0:
if (j == null) return;
print(j.isEven);
j = k;
continue label;
}
}
In catch after possible write in try
#
The cause:
The variable might have been written to in a
try block, and execution is now in a
catch block.
Example:
void f(int? i, int? j) {
if (i == null) return;
try {
i = j; // (1)
// ... Additional code ...
if (i == null) return; // (2)
// ... Additional code ...
} catch (e) {
print(i.isEven); // (3) ERROR
}
}
In this case, flow analysis doesn't consider
i.isEven (3) safe, because it has no way of
knowing when in the try block the exception
might have occurred, so it conservatively assumes that it
might have happened between (1) and (2), when
i was potentially null.
Similar situations can occur between try and
finally blocks, and between
catch and finally blocks. Because
of a historical artifact of how the implementation was done,
these try/catch/finally
situations don't take into account the right-hand side of
the assignment, similar to what happens in loops.
Solution:
To fix the problem, make sure that the
catch block doesn't rely on assumptions about
the state of variables that get changed inside the
try block. Remember, the exception might occur
at any time during the try block, possibly when
i is null.
The safest solution is to add a null check inside the
catch block:
try {
// ···
} catch (e) {
if (i != null) {
print(i.isEven); // (3) OK due to the null check in the line above.
} else {
// Handle the case where i is null.
}
}
Or, if you're sure that an exception can't occur while
i is null, just use the
! operator:
try {
// ···
} catch (e) {
print(i!.isEven); // (3) OK because of the `!`.
}
Subtype mismatch
#The cause: You're trying to promote to a type that isn't a subtype of the variable's current promoted type (or wasn't a subtype at the time of the promotion attempt).
Example:
void f(Object o) {
if (o is Comparable /* (1) */ ) {
if (o is Pattern /* (2) */ ) {
print(o.matchAsPrefix('foo')); // (3) ERROR
}
}
}
In this example, o is promoted to
Comparable at (1), but it isn't promoted to
Pattern at (2), because
Pattern isn't a subtype of
Comparable. (The rationale is that if it did
promote, then you wouldn't be able to use methods on
Comparable.) Note that just because
Pattern isn't a subtype of
Comparable
doesn't mean the code at (3) is dead;
o might have a type—like
String—that implements both
Comparable and Pattern.
Solution:
One possible solution is to create a new local variable so
that the original variable is promoted to
Comparable, and the new variable is promoted to
Pattern:
void f(Object o) {
if (o is Comparable /* (1) */ ) {
Object o2 = o;
if (o2 is Pattern /* (2) */ ) {
print(
o2.matchAsPrefix('foo'),
); // (3) OK; o2 was promoted to `Pattern`.
}
}
}
However, someone who edits the code later might be tempted
to change Object o2 to var o2.
That change gives o2 a type of
Comparable, which brings back the problem of
the object not being promotable to Pattern.
A redundant type check might be a better solution:
void f(Object o) {
if (o is Comparable /* (1) */ ) {
if (o is Pattern /* (2) */ ) {
print((o as Pattern).matchAsPrefix('foo')); // (3) OK
}
}
}
Another solution that sometimes works is when you can use a
more precise type. If line 3 cares only about strings, then
you can use String in your type check. Because
String is a subtype of Comparable,
the promotion works:
void f(Object o) {
if (o is Comparable /* (1) */ ) {
if (o is String /* (2) */ ) {
print(o.matchAsPrefix('foo')); // (3) OK
}
}
}
Write captured by a local function
#The cause: The variable has been write-captured by a local function or function expression.
Example:
void f(int? i, int? j) {
var foo = () {
i = j;
};
// ... Use foo ...
if (i == null) return; // (1)
// ... Additional code ...
print(i.isEven); // (2) ERROR
}
Flow analysis reasons that as soon as the definition of
foo is reached, it might get called at any
time, therefore it's no longer safe to promote
i at all. As with loops, this demotion happens
regardless of the type of the right-hand side of the
assignment.
Solution:
Sometimes it's possible to restructure the logic so that the promotion is before the write capture:
void f(int? i, int? j) {
if (i == null) return; // (1)
// ... Additional code ...
print(i.isEven); // (2) OK
var foo = () {
i = j;
};
// ... Use foo ...
}
Another option is to create a local variable, so it isn't write captured:
void f(int? i, int? j) {
var foo = () {
i = j;
};
// ... Use foo ...
var i2 = i;
if (i2 == null) return; // (1)
// ... Additional code ...
print(i2.isEven); // (2) OK because `i2` isn't write captured.
}
Or you can do a redundant check:
void f(int? i, int? j) {
var foo = () {
i = j;
};
// ... Use foo ...
if (i == null) return; // (1)
// ... Additional code ...
print(i!.isEven); // (2) OK due to `!` check.
}
Written outside of the current closure or function expression
#The cause: The variable is written to outside of a closure or function expression, and the type promotion location is inside the closure or function expression.
Example:
void f(int? i, int? j) {
if (i == null) return;
var foo = () {
print(i.isEven); // (1) ERROR
};
i = j; // (2)
}
Flow analysis reasons that there's no way to determine when
foo might get called, so it might get called
after the assignment at (2), and thus the promotion might no
longer be valid. As with loops, this demotion happens
regardless of the type of the right-hand side of the
assignment.
Solution:
A solution is to create a local variable:
void f(int? i, int? j) {
if (i == null) return;
var i2 = i;
var foo = () {
print(i2.isEven); // (1) OK because `i2` isn't changed later.
};
i = j; // (2)
}
Example:
A particularly nasty case looks like this:
void f(int? i) {
i ??= 0;
var foo = () {
print(i.isEven); // ERROR
};
}
In this case, a human can see that the promotion is safe
because the only write to i uses a non-null
value and happens before foo is ever created.
But
flow analysis isn't that smart.
Solution:
Again, a solution is to create a local variable:
void f(int? i) {
var j = i ?? 0;
var foo = () {
print(j.isEven); // OK
};
}
This solution works because j is inferred to
have a non-nullable type (int) due to its
initial value (i ?? 0). Because
j has a non-nullable type, whether or not it's
assigned later, j can never have a non-null
value.
Write captured outside of the current closure or function expression
#The cause: The variable you're trying to promote is write captured outside of a closure or function expression, but this use of the variable is inside of the closure or function expression that's trying to promote it.
Example:
void f(int? i, int? j) {
var foo = () {
if (i == null) return;
print(i.isEven); // ERROR
};
var bar = () {
i = j;
};
}
Flow analysis reasons that there's no way of telling what
order foo and bar might be
executed in; in fact, bar might even get
executed halfway through executing foo
(due to foo calling something that calls
bar). So it isn't safe to promote
i at all inside foo.
Solution:
The best solution is probably to create a local variable:
void f(int? i, int? j) {
var foo = () {
var i2 = i;
if (i2 == null) return;
print(i2.isEven); // OK because i2 is local to this closure.
};
var bar = () {
i = j;
};
}
Promotion lost across an await or
yield
#
The cause:
Flow analysis drops type promotion when execution suspends
across an await expression or
yield statement.
For this promotion failure to occur, the following conditions must be met:
- A function or method contains a write to a local variable or parameter.
- That function contains an inner function or closure that promotes the variable.
-
The inner function suspends itself using
awaitoryield, letting the outer function continue execution. - The inner function attempts to use the local variable again after the suspension.
In earlier SDK releases, using the local variable after the suspension retained its promoted type. However, that behavior was unsound: while the inner function is suspended, the outer function can continue running and modify the variable's value. Starting in Dart 3.13, to guarantee soundness, flow analysis drops the promotion when the inner function suspends.
Example:
The compiler drops a promotion across suspension points, even if it's clear that retaining the promotion is safe.
import 'dart:async';
Future<void> example(String? extraInfo) async {
extraInfo ??= 'No extra info'; // (1)
unawaited(() async {
log('Doing some asynchronous task...');
log(extraInfo!);
await longTask(); // (2)
log('Done!');
log(extraInfo); // (3) ERROR
}());
}
Future<void> longTask() async {
await Future<void>.delayed(const Duration(seconds: 10));
}
void log(String s) {
print(s);
}
In this example, assigning a value to
extraInfo at (1) means it isn't
effectively final. Flow analysis conservatively assumes that
any modified variable captured by a closure might change
while the closure is suspended at (2), even
though the assignment at (1) always happens
before the closure runs. To ensure soundness, flow analysis
drops the promotion. As a result, at (3),
extraInfo demotes back to String?,
which isn't compatible with log(String s) and
produces a compile-time error.
Message:
When this situation occurs, the compiler or analyzer emits an error and context message:
example.dart:10:9: Error: The argument type 'String?' can't be assigned to the parameter type 'String'.
log(extraInfo);
^
example.dart:8:5: Context: Variable 'extraInfo' could not be promoted due to an 'await' or 'yield'.
Try checking the type of the variable after the 'await' or 'yield'. See http://dart.dev/go/non-promo-suspension
await longTask();
^
Solution:
To fix this compile-time error, re-verify the variable's
non-null type, add an explicit null check, or use a not-null
assertion (!) after the suspension point:
import 'dart:async';
Future<void> example(String? extraInfo) async {
extraInfo ??= 'No extra info';
unawaited(() async {
log('Doing some asynchronous task...');
log(extraInfo!);
await longTask();
log('Done!');
log(extraInfo!);
}());
}
Future<void> longTask() async {
await Future<void>.delayed(const Duration(seconds: 10));
}
void log(String s) {
print(s);
}
Alternatively, assign the variable to a
final local variable prior to suspension, or
re-check extraInfo != null after the
await expression.