Adopting Dart Sound Null Safety to Eliminate Runtime Null Dereferences
Learn how Dart’s sound null safety works, how to migrate existing code, and where the safety guarantees can still be broken if misused.
14 Sept 2026, 08:19 UTC

The problem: silent null dereferences in pre‑null‑safety Dart
Before Dart 2.12, variables could hold null unless you explicitly checked for it. A missed check led to a NoSuchMethodError (the Dart equivalent of a null‑pointer exception) that only surfaced at runtime, often after the app had shipped. Debugging these errors required tracing the call stack back to a missing guard, which added friction to feature work and increased the chance of shipping a crash.
Thesis: sound null safety removes an entire class of runtime null‑dereference errors
Dart’s sound null safety makes every variable non‑nullable by default. You must annotate a type with ? to allow null. The Dart analyzer then enforces soundness: if your code compiles without null‑safety warnings, the compiler guarantees that no null‑dereference can occur at execution time. This shifts the burden from runtime testing to static analysis, letting you catch the problem while you write code.
Section 1: Core concepts of sound null safety
When you create a new Dart project with SDK ≥2.12, the generated files already use non‑nullable types. For example:
// main.dart
void main() {
String name = 'Alice'; // non‑nullable, cannot be null
String? maybeName = null; // explicitly nullable
// The following line would cause an analyzer error:
// String broken = maybeName; // error: A value of type 'String?' can't be assigned to a variable of type 'String'.
}
The analyzer flags the assignment because maybeName might be null. To use the value you must first check or use a null‑aware operator:
String safeName = maybeName ?? 'Unknown'; // uses ?? to provide a fallback
int length = maybeName?.length ?? 0; // ?. prevents dereference on null
If you are absolutely certain a nullable variable holds a non‑null value at a specific point, you can use the non‑null assertion operator !!, but this re‑introduces the possibility of a runtime error if the assumption is wrong.
Section 2: Migrating an existing codebase with dart migrate
For projects that predate null safety, the migration tool automates the addition of ? and late while trying to preserve the original behavior.
- Prepare: Ensure you are using Dart SDK ≥2.12 and have committed your current state (e.g., via Git).
- Run the migration: From the project root, execute:
dart migrate --apply-changes
The tool walks through each file, proposes edits, and writes them directly to disk. After the migration finishes, run the analyzer to see whether any warnings remain:
dart analyze
If the analyzer reports no null‑safety warnings, the migration succeeded for the parts it could handle safely. However, the tool may produce false positives in areas that rely on reflection (dart:mirrors) or dynamic types; those sections will need manual inspection.
Section 3: Using null‑aware operators wisely
Null‑safe code often combines ?., ??, and !!. Consider a function that fetches a user’s email from a map that might be missing keys:
String? getUserEmail(Map? data) {
return data?['user']?['email'] as String?;
}
void printEmail(Map? payload) {
final email = getUserEmail(payload);
// Option 1: Provide a default when null
print('Email: ${email ?? "not provided"}');
// Option 2: Assert non‑null (risky)
// print('Email: ${email!!}'); // throws if email is null
}
The first option uses the null‑coalescing operator ?? to supply a fallback, keeping the program safe. The second option uses !! and will throw if email is actually null. Over‑reliance on !! erodes the safety guarantee, so treat it as a last resort after you have validated the assumption elsewhere (e.g., via an earlier check).
Trade‑offs and limitations
- Over‑use of
!!: As noted, each!!re‑introduces the possibility of a runtime null error. Teams should adopt a lint rule (e.g.,avoid_unnecessary_containers‑style) that flags non‑null assertions unless justified. - Unsound null safety mode: If you depend on a package that has not been migrated, you can opt into unsound mode via
dart run --unsound-null-safety. This disables the analyzer’s soundness checks and re‑opens the risk of null dereferences, so use it only as a temporary workaround. - Reflection and
dart:mirrors: Code that inspects types at runtime may break after migration because the type system now encodes nullability. Such code often requires manual adjustments or a move todart:reflectable.
To verify that your migration has not introduced regressions, run your test suite after migration and after any manual fixes. If tests pass and dart analyze shows zero null‑safety warnings, you have a strong indication that the codebase respects sound null safety.
Actionable closing
- Start a new project with
dart create --template=console-simple my_appand observe that the generated code is already null‑safe. - For an existing project, run
dart migrate --apply-changesin a branch, then check the analyzer output. - Replace ad‑hoc null checks with
?.and??where appropriate, and reserve!!for cases where you have documented guarantees. - Add a lint rule to your
analysis_options.yamlthat warns on unnecessary non‑null assertions, helping the team keep the safety guarantee intact.
By following these steps, you leverage Dart’s sound type system to eliminate a whole class of runtime crashes, making your apps more reliable and reducing debugging time.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.