Null Safety: Living Without Surprises
What "null" means, and why it causes trouble
Before we get to the fix, we need to understand the problem.
In most programming languages, a variable can hold a special value called null. Null means "nothing at all" — no text, no number, no object. It is the programming equivalent of an empty envelope. The envelope exists, but there is no letter inside.
The trouble starts when a program tries to use the empty envelope as if it had a letter in it. Imagine this instruction: "Take the name written on the envelope and print it." If the envelope is empty, the program has no idea what to do. In many languages, the result is a crash — the program stops suddenly with an error like "null reference exception." This kind of crash is so common that its inventor once called it his "billion-dollar mistake," because companies have spent enormous amounts of time and money fixing it.
Dart's answer is called null safety. The idea is simple and powerful: the Dart compiler — the program that checks your code before it runs — refuses to let you use a value as if it exists when it might not. Instead of crashing at runtime (while the program is running), you find out about the problem earlier, while writing the code.
Two kinds of variables
With null safety, every type in Dart comes in two flavors:
String— a variable that is guaranteed to hold text. Never null. Always safe to use.String?— a variable that holds either text or null. The question mark is a signal: "this might be empty."
The same pattern works for every type: int?, bool?, double?, and so on.
String name = 'Ada'; // guaranteed to be text
String? nickname; // starts as null — no nickname yet
print(name); // fine: name definitely has text
print(nickname); // fine: printing null is allowed
print(nickname.length); // ERROR: Dart stops you right hereThat last line is the heart of null safety. With an ordinary String, asking for its length is always fine. With a String?, the compiler says: "Wait — how do you know there is a string there at all?" You are not allowed to continue until you answer that question in code.
Four tools for working with "maybe null" values
Dart gives you a small toolkit of operators, each with its own symbol. Let's go through them one by one.
1. The null-aware operator `??` — "use this instead"
The double question mark reads as "otherwise." If the value on the left is null, use the value on the right.
String? maybeName;
print(maybeName ?? 'guest'); // prints: guest2. The null-aware assignment `??=` — "set it only if it's empty"
This assigns a new value, but only when the current one is null.
String? maybeName;
maybeName ??= 'Ada'; // now it holds 'Ada'
maybeName ??= 'Grace'; // no effect — it already has a value
print(maybeName); // prints: Ada3. The safe navigation operator `?.` — "only do this if it exists"
A normal dot (.) says "do this action on the value." The ?. says "do it if the value exists; otherwise the whole expression quietly becomes null."
String? maybeName = 'Ada';
int length = maybeName?.length ?? 0; // 3Notice how the two tools combine: maybeName?.length is null if maybeName is null, and then ?? 0 supplies a fallback of zero. This "maybe a value, maybe a fallback" chain is extremely common in real Dart code.
4. The bang operator `!` — "I promise it's there"
Sometimes you know something the compiler cannot prove. The exclamation mark tells Dart: "Trust me, this is not null. If I'm wrong, crash on purpose."
String? maybeName = 'Ada';
print(maybeName!.length); // 3 — works because we just set itThe ! operator should be rare. It puts the risk of a crash back in your hands, which is exactly what null safety tries to avoid. Use it only when you can clearly explain why the value cannot be null at that moment.
callout note The four symbols at a glance:
String?— this variable may be null.a ?? b— useaunless it's null, then useb.a ??= b— assignbtoaonly ifais currently null.a?.b— accessbonly ifais not null.a!.b— accessband crash ifais null (use sparingly).
Null safety in the wider flow of a program
Here is how the compiler sees your code. Every time you touch a nullable value, it checks whether you have made the value safe first.
caption: How the compiler decides whether a nullable value can be used safely. alt: A flow diagram starting at declaring a nullable string, followed by a decision about whether null-ness has been ruled out. If not, a compiler error is shown leading to a fix step that loops back to the decision; if yes, the value can be used safely.
One of those fixes deserves special mention: a plain if check. Once Dart sees code that proves a value is not null, it remembers for the rest of that region of code.
String? nickname;
if (nickname != null) {
print(nickname.length); // allowed — Dart knows it's not null here
}This is called flow analysis: the compiler follows the flow of your program the way a careful reader would, and only complains when the proof is missing.
Declaring values that start empty but fill up soon
Sometimes a variable genuinely cannot have a value at the moment of declaration, but you know it will get one before anyone uses it. The keyword late handles this: it tells Dart "I will fill this in before reading it. Check me at runtime, not at compile time."
late String configFile;
void loadSettings() {
configFile = 'app.config'; // filled in before anyone reads it
}
void main() {
loadSettings();
print(configFile); // fine
}If you forget to call loadSettings() first, you get a clear, immediate error saying the value was read too early — much easier to diagnose than a mysterious null appearing somewhere later.
callout warning late and ! shift safety checks from compile time to runtime. They are useful escape hatches, but every use is a place where a crash becomes possible again. Prefer ??, ?., and plain null checks whenever you can.
Putting it together: a small worked example
void main() {
// A user profile where every field is optional.
String? email;
String? phone;
// The user later adds an email but no phone.
email ??= 'ada@example.com';
// Build a contact line safely.
final contact = [
email ?? 'no email',
phone ?? 'no phone',
].join(', ');
print(contact); // ada@example.com, no phone
print('email length: ${email?.length ?? 0}');
print('phone length: ${phone?.length ?? 0}'); // 0, no crash
}Notice how the whole program runs without a single ! and without a single if (x != null). That is the goal of null safety: expressing "maybe missing, here is what to do instead" directly and briefly.
exercise Q: You have String? city;. Write one line that prints "unknown" if city is null and the city's name otherwise. A: print(city ?? 'unknown'); — the ?? operator substitutes "unknown" whenever city holds null.
exercise Q: Rewrite print(name!.length); without using the ! operator, keeping the behavior safe when name is null. A: print(name?.length ?? 0); — safe navigation yields null when name is null, and ?? 0 turns that into a harmless zero.
exercise Q: What happens if you read a late variable before assigning it? A: The program fails at runtime with an error stating the late variable was used before being initialized — the check happens when the program runs, not when it compiles.
estimate minutes: 45
Spotted a mistake, or code that does not work?