Null Safety in Depth
A closer look at ?, !, and late — how Dart eliminates null-reference errors at compile time instead of runtime.
3 min de lectura
You've already seen the basics: a type without ? can never be null, and one with ? might be. This is sound null safety — "sound" meaning the compiler can guarantee a non-nullable variable is never null, not just discourage it by convention. It's worth understanding why that guarantee is such a big deal, and the tools Dart gives you to work within it.
The problem it solves
In languages without this guarantee, almost any variable can secretly be null, and the only way to find out is to hit a crash at runtime — often called the "billion-dollar mistake" by the inventor of the null reference himself. Dart closes this off at compile time:
String name = 'Dara';
// name = null; // compile error — String can never be null
String? nickname;
print(nickname.length); // compile error — must check for null firstThat second error is the important one: Dart won't let you call a method on a possibly-null value without proving, one way or another, that it isn't null at that point.
Narrowing with if
The simplest way to "prove" non-null is a plain if check — Dart's analyzer tracks this automatically:
String? nickname = 'Dee';
if (nickname != null) {
print(nickname.length); // fine — Dart knows it's a String here, not String?
}Inside the if block, Dart treats nickname as a definite String, not String?, purely from the surrounding check. This is called type promotion, and it's why so much null-safe Dart code doesn't need explicit unwrapping at all.
The null assertion operator (!)
Sometimes you know a value isn't null even though the type system can't prove it — for instance, right after checking a Map for a key. The ! operator asserts that:
Map<String, String> config = {'env': 'production'};
String env = config['env']!; // "trust me, this key exists"Use ! sparingly. If you're wrong, it throws a runtime exception immediately — which is exactly the crash null safety was designed to prevent elsewhere. Prefer ?? for a safe fallback, or an explicit if check, and reach for ! only when you have real certainty (like a key you just inserted yourself two lines above).
late: deferring initialization
late tells Dart "this non-nullable variable will be set before it's used, even though I'm not setting it right now":
class UserSession {
late String token; // no value yet
void login(String receivedToken) {
token = receivedToken;
}
void printToken() {
print(token); // safe, as long as login() ran first
}
}Without late, String token; with no initializer would be a compile error — every non-nullable field normally needs a value immediately. late is the escape hatch for cases where the value genuinely arrives later (from a constructor's body, an async call, or a framework lifecycle method) but you don't want to make the field nullable just to satisfy the compiler. The tradeoff: if you read a late variable before it's ever assigned, Dart throws a runtime error — so it shifts the guarantee from "compiler-checked" back to "your responsibility," and should be used only when you're confident about the ordering.
The mental model
Null safety isn't a set of syntax tricks to memorize — it's the compiler forcing every "this might not exist" case into the open, at the exact place you'd otherwise forget about it. The small amount of extra syntax (?, !, ??, late) buys you the near-total elimination of a bug class that, in unsafe languages, is often the single most common cause of production crashes.