Missing values and calls that fail
Types that say a value may be absent, the safe call and the fallback that handle one, why the bang is the last resort, and how throw with try and catch keeps a failure from ending the program.
Saying that a value may be absent
A plain String is always there. Add a question mark and you have said the value may be missing: String? nickname. That mark is a promise in the other direction — the compiler now refuses to treat the value as text until you have dealt with the possibility that it is null.
Null is a real value and a useful one: it is what a map gives you for a key that is not there, and what an optional field holds when nobody filled it in. The work is in deciding what a missing value should mean at the moment you use it, rather than hoping it never happens.
The safe call and the fallback
?. is the safe call: nickname?.length asks for the length only when there is something to ask about, and the whole expression answers null when there is not. Nothing after the safe call runs, so a chain of them cannot crash on a missing value.
?? supplies the fallback: nickname ?? "none" gives the nickname when it is there and the text you chose when it is not. prices["mouse"] ?? 0 is the same idea for a lookup, and it is what turns a missing price into a number a report can add up.
A value stored in a field is not one the compiler can vouch for, because another line could change it at any time. Copy it into a local first — final nick = nickname; — and the compiler can see for itself that the local is not null below your check. That is usually the tidiest answer, and it needs no bang at all.
The bang, and what to do when a call fails
! tells the compiler to trust you: prices["hub"]! is read as a value that is certainly there. It is the last resort, because it is a claim rather than a check, and when the claim is wrong the program stops with an error. Use it where the value is undeniably present, and prefer ?? everywhere else.
Some failures cannot be designed away. A function can raise one on purpose with throw: throw ArgumentError("no price") stops the ordinary flow and starts looking for someone to deal with it. try marks the lines that might fail and catch holds the answer to the failure, so the program keeps running and can print a sentence a person can act on.
The compiler follows your checks
Null safety is not only a mark on a type; the compiler tracks what it knows at each line. After if (nick == null) return; the name nick is known to be non-null below the check, so .length is allowed there. This is flow analysis, and it is why a check followed by a use needs no bang.
The tracking works best for local variables. A field can be changed by another line between the check and the use, so the compiler will not keep the promotion for it. Copy the field into a local, check the local, and use the local — the guarantee then holds for the whole block.
late is the promise that a non-nullable field will be filled before it is read. It moves the check to run time: read it too early and the program throws. Use it when the constructor cannot supply the value, not as a way to avoid deciding what a missing value means.
Failures are values you can catch
throw takes an object and starts unwinding the call stack until a catch takes it. ArgumentError and FormatException are in the standard library, and the message on them is what the reader of the output will see. Throw the most specific type that describes the problem.
on FormatException catch (error) catches only that type, while a bare catch catches everything. The narrow form is better: a bug such as a null read should not be swallowed by the handler you wrote for bad input. catch gives you the object, and its toString() is what a message should include.
finally runs whether the try succeeded, threw or returned early. It is for the step that must happen either way, such as printing a closing line or releasing a resource. A rethrow inside a catch passes the same failure on after you have recorded it.
A field that may be missing, handled with a local
class Profile {
Profile(this.name, this.nickname);
final String name;
final String? nickname;
String get display {
final nick = nickname;
if (nick == null) {
return name;
}
return name + " aka " + nick;
}
}
void main() {
print(Profile("Ada", null).display);
print(Profile("Grace", "amazing").display);
}A missing price, and a call that fails
Run it and read the three ways a missing value is handled. Then ask the map for a key you know is absent with both the fallback and the safe call, and change the caught item so the error text names a different product.
Three null and error mistakes. Putting ! on every nullable value to silence the compiler, which turns a check into a crash. Catching everything with a bare catch and printing a friendly line, which hides the bug you needed to see. And comparing a nullable number with > before checking it — the compiler refuses, and the fix is a fallback or a local, not a bang.
Try these next
- Ask a map for a missing key three ways: print the raw value, use
??, and use?.. - Throw an ArgumentError from a function, catch it, and print the message it carries.
- Add a
finallyline to a try that returns early and confirm that it still runs.
Three prices, one of them broken
Run it and read which inputs take the fallback and which are caught. Then add a fifth input of your own, one with spaces around the number, and see whether it parses.
Every bang you write is a promise that the value is there. When one turns out to be wrong, the program stops on that line — which is why a fallback is the calmer answer wherever a missing value is possible.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does the question mark in String? tell the compiler?
