Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Dart (3.13)  Module 4 – Sound null safety

Sound null safety: nullable types and operators

What sound null safety guarantees and since when, how to replace a risky null assertion, the null-aware operators, and Dart 3.8 null-aware elements.

  • Beginner
  • 10 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain what sound null safety guarantees and since when Dart enforces it
  • Rewrite a ! that may throw into a safe check with ??, promotion or an if-case
  • Use the null-aware operators ?., ?[], ?? and ??=
  • Build collections with Dart 3.8 null-aware elements

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain what sound null safety guarantees and since when Dart enforces it
  • Rewrite a ! that may throw into a safe check with ??, promotion or an if-case
  • Use the null-aware operators ?., ?[], ?? and ??=
  • Build collections with Dart 3.8 null-aware elements

Before you start

On this page

In Dart a value whose type is String can never be null; only String? admits it. That one rule, enforced by the compiler, is sound null safety, and it turns a whole class of run-time crashes into errors you see before the program runs. The cost is a handful of operators for the places where a value genuinely might be absent.

You will see what non-nullable means and since when Dart enforces it, read the error a ! throws and three safer ways to write it, use the null-aware operators, and build a query whose unset filters simply disappear with Dart 3.8 null-aware elements.

Non-nullable by default

A type is non-nullable unless you add ?. String name must always hold a string; String? note may hold a string or null. This is sound null safety: Dart's guarantee that a value whose type is not nullable is never null, checked by the compiler and still true while the program runs. A type allows null only when it says so, as String? does. More in “What Dart is and where it runs”, and “sound” is the strong word: the understanding-null-safety page explains that a value whose static type is non-nullable can never be null at run time, which also lets the compiler drop null checks it knows can never fire. Dart has enforced it since Dart 3, which arrived in 2023 and dropped the language versions before 2.12. The history is short but worth seeing:

Timeline · 4 events in order Sound null safety across Dart versions Walk the versions to see how null safety became the default and then grew.
  1. 2.12

    Null safety, opt-in opt-in

    Types became non-nullable by default for packages that opted in, with String? for values that may be null and the sound guarantee behind it.

  2. 3.0

    Enforced for everyone mandatory

    Dart 3, released in 2023, made sound null safety mandatory and dropped support for language versions before 2.12, so every program is now null-safe.

  3. 3.8

    Null-aware elements new syntax

    Collections gained ?expr, which drops a list element or map entry when it is null, so building a query from optional filters needs no if per entry.

  4. 3.9

    Flow analysis assumes null safety analysis

    Promotion, reachability and definite assignment now assume null safety, so an upgrade may surface more dead_code warnings where a null check can never fail.

The timeline has 4 events, from 2.12 to 3.9.

Because the guarantee is sound rather than best-effort, you can trust a non-nullable type the way you trust an int to be a number: the type system has already ruled out the absent case.

What you give up is the freedom to leave a value unset by accident; what you gain is that the compiler, not a run-time crash, tells you where a value might be missing. A field that can legitimately be absent is declared nullable, such as String? middleName, and every reader then has to handle the null before using it. A field that must always be present stays non-nullable, and the compiler refuses any path that could leave it null. This turns “could this be null here?” from a question you carry in your head into one the analyzer answers, and it is why reading a ? in a signature is useful information rather than noise.

Checkpoint

Check: Non-nullable by default

  1. Question 1 of 2 Which type admits null in Dart?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: String?

    String must always hold a string; only String? may be null. That default is the heart of null safety.

  2. Question 2 of 2 What does the word sound add to null safety?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A non-nullable value can never be null at run time, not just usually

    Understanding null safety states that a non-nullable value is never null at run time, which lets compilers optimise.

The null-assertion operator can throw

The ! operator takes a nullable value and asserts it is not null, giving back the non-nullable type. When the value is null, it throws. bang.dart reads a key that is not in the map and asserts it:

bang.dart: ! on a missing map key Dart · bang.dart
// The ! operator asserts that a value is not null. When it is, the program
// throws at run time.
void main() {
  final prices = {'tea': 10};
  final coffee = prices['coffee']!;
  print(coffee);
}

Output (exit status 255)

Printed as an error (standard error)

Unhandled exception:
Null check operator used on a null value
#0      main (bang.dart:5:34)
#1      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19)
#2      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run bang.dart

It threw Null check operator used on a null value and ended with status 255. Each ! is a promise the compiler cannot check, so outside tests it is a code smell: it says “I know better than the type system”, and a wrong guess crashes. safe.dart replaces it three ways:

safe.dart: ??, promotion and an if-case instead of ! Dart · safe.dart
// Three safe ways to read a possibly-missing value instead of the ! operator.
void main() {
  final prices = {'tea': 10};

  // 1. ?? supplies a fallback when the lookup is null.
  final coffee = prices['coffee'] ?? 0;
  print('coffee: $coffee');

  // 2. An if that checks for null promotes the variable to a non-null type.
  final tea = prices['tea'];
  if (tea != null) {
    print('tea: ${tea + 5}');
  }

  // 3. An if-case pattern binds only when the value is not null.
  if (prices['tea'] case final t?) {
    print('tea again: $t');
  }
}

Output

coffee: 0
tea: 15
tea again: 10

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run safe.dart

It printed coffee: 0, tea: 15 and tea again: 10. The ?? operator supplied 0 when the lookup was null; assigning the lookup to tea and testing if (tea != null) promoted tea to a plain int inside the block; and the if-case prices['tea'] case final t? bound t only because the value was not null. Step through the contrast:

Interactive · step by step A missing key is null, so handle it or it throws Step through reading prices['coffee'] when there is no coffee key, three ways.
A missing key is null, so handle it or it throws, step 4 of 4. Showing prices['coffee'], int? is null, the ! operator, throws at run time, ?? 0 fallback, 0. In focus: prices['coffee'].prices['coffee']int? is nullthe ! operatorthrows at run time?? 0 fallback0

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. The lookup is nullable. A map lookup returns a nullable type. prices['coffee'] has type int?, and the coffee key is missing, so the value is null.
  2. asserts it is not null. Writing prices['coffee']! tells the compiler the value is not null. Because it is, bang.dart threw Null check operator used on a null value and stopped.
  3. ?? supplies a fallback. prices['coffee'] ?? 0 gives 0 when the lookup is null, so safe.dart printed coffee 0 and kept running. The fallback replaces the ! and cannot throw.
  4. Or promote with an if. Assigning the lookup to a local and testing it with if (tea != null) promotes tea to a plain int inside the block, so tea + 5 needs no ! at all.

Checkpoint

Check: The null-assertion operator

  1. Question 1 of 2 What does the ! operator do to a nullable value?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Asserts it is not null and throws at run time if it is

    bang.dart asserted a missing key and threw Null check operator used on a null value, status 255.

  2. Question 2 of 2 A value read as prices[k] may be null. Which rewrite of prices[k]! cannot throw?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: prices[k] ?? 0

    safe.dart used ?? to print coffee 0 when the key was missing, instead of asserting with !.

Null-aware operators

For the places where null is expected, Dart has a family of null-aware operator: An operator that works on a value that may be null: ?? and ??= supply a fallback, and ?. and ?.. skip the member access or the cascade when the receiver is null. More in “Operators, cascades and equality in Dart” that do nothing, or supply a value, when the receiver is null. They save the nested if checks that clutter code in languages without them. This table lists the ones you reach for most:

Operator On a null receiver Example
?. skips the member access, gives null user?.name
?[] skips the index, gives null list?[0]
?? uses the right-hand fallback name ?? 'guest'
??= assigns only if currently null cache ??= compute()
...? spreads only if the collection is non-null [...?extra]
?.. skips the cascade builder?..add(x)

The map lookup in safe.dart returned a nullable int, which is exactly where ?? fits; m['k'] ?? fallback is the everyday replacement for m['k']!. Reach for containsKey or an if-case only when null is itself a valid stored value and you must tell “missing” from “present and null” apart.

Checkpoint

Check: Null-aware operators

  1. One question What does user?.name do when user is null?

    Choose one answer.

    Show the answer: One question

    Answer: Skips the access and evaluates to null

    The null-aware operators act only when the receiver is non-null, so ?. gives null on a null receiver.

Null-aware elements build collections

Dart 3.8 extended null-awareness into collection literals. A ?expr element is included only when it is not null, so a list or map built from optional parts drops the missing ones without an if for each. searchparams.dart builds a query this way:

searchparams.dart: null-aware list elements and map entries Dart · searchparams.dart
// Null-aware elements (Dart 3.8): ?expr in a collection drops the element when
// it is null, so unset filters simply disappear.
void main() {
  String? coupon;
  const order = 'order-42';
  const gift = 'gift';
  final items = [order, ?coupon, gift];
  print(items);

  String? city;
  const q = 'chai';
  final params = {
    'q': q,
    'city': ?city,
  };
  print(params);
}

Output

[order-42, gift]
{q: chai}

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run searchparams.dart

The list [order, ?coupon, gift] printed [order-42, gift]: coupon was null, so ?coupon contributed nothing. The map wrote 'city': ?city, and because city was null the whole city entry vanished, leaving {q: chai}. This is how you turn a set of optional filters into a query with no empty keys: each optional value is a null-aware element, and only the ones that are set appear.

Which null-aware form to use

Use ...? to splice in a whole list that might be null, and ?expr for a single element or entry that might be null. Both leave the result free of nulls, so the collection’s type stays non-nullable.

Checkpoint

Check: Null-aware elements

  1. One question In a Dart 3.8 list, what does ?coupon contribute when coupon is null?

    Choose one answer.

    Show the answer: One question

    Answer: Nothing; the element is dropped

    searchparams.dart built [order-42, gift] because ?coupon was dropped when coupon was null.

Never, and the error that keeps nulls out

The rule has teeth at compile time. assign_error.dart tries to put a String? into a String:

assign_error.dart: a nullable value in a non-null variable Dart · assign_error.dart
// A nullable value cannot be assigned to a non-nullable variable; the compiler
// rejects it.
String shout(String? maybe) {
  String sure = maybe;
  return sure.toUpperCase();
}

void main() => print(shout('asha'));

Output (exit status 254)

Printed as an error (standard error)

assign_error.dart:4:17: Error: A value of type 'String?' can't be assigned to a variable of type 'String'.
  String sure = maybe;
                ^

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run assign_error.dart

The compiler refused it with “A value of type ‘String?’ can’t be assigned to a variable of type ‘String’” and status 254: you must handle the null first. At the other end sits Never, the type of an expression that never completes normally, such as a function that always throws. never.dart uses one:

never.dart: a Never-returning helper and the promotion it allows Dart · never.dart
// A function that always throws returns Never. Flow analysis knows the code
// after a Never call is unreachable, so n is a non-null int below the if.
Never fail(String message) => throw StateError(message);

int parsePort(String s) {
  final n = int.tryParse(s);
  if (n == null) fail('not a number: $s');
  return n;
}

void main() {
  print(parsePort('8080'));
  try {
    parsePort('oops');
  } on StateError catch (e) {
    print('caught: ${e.message}');
  }
}

Output

8080
caught: not a number: oops

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run never.dart

It printed 8080, then caught: not a number: oops. Because fail returns Never, the compiler knows the code after if (n == null) fail(...) runs only when n is not null, so n is promoted to a plain int at return n, with no !. Never is how a throw-helper keeps flow analysis happy, a thread type promotion, late and definite assignment picks up in full.

Checkpoint

Check: Never and the assignment rule

  1. Question 1 of 2 What happens when you assign a String? to a String variable?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A compile error; you must handle the null first

    assign_error.dart failed with status 254 and A value of type 'String?' can't be assigned to a variable of type 'String'.

  2. Question 2 of 2 Why does Never let n be used as a non-null int after if (n == null) fail(...)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: fail returns Never, so the code after it runs only when n is not null

    never.dart returned 8080 because n was promoted to int after the Never-returning fail could not return.

Practise: remove every !

Take a function that over-uses ! and make it safe.

  1. Start from a small order-summary function that reads several nullable fields with !, such as order.coupon! and order.note!, and currently crashes when any is missing.
  2. Replace each !: use ?? for a fallback, promotion with an if where the value is used several times, and a null-aware element when building the summary list.
  3. Run it with every field set, then with each field null in turn, and confirm it never throws.
  4. As a check, make sure dart analyze reports no use of ! left in the function.

When it survives every null, the self-check revisits each section:

Practice that adapts to you

Practise this lesson

5 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 5 Since which Dart release has sound null safety been mandatory?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: Dart 3

    Null safety was opt-in from 2.12 and enforced for everyone in Dart 3.

  2. Question 2 of 5 Where is a ! defensible rather than a smell?

    Choose one answer.

    Show the answer: Question 2 of 5

    Answer: In a test, or right after a check the analyzer cannot follow

    Each ! is an unchecked promise; keep it to tests or a provable-but-unanalysable case, and prefer ?? or promotion elsewhere.

  3. Question 3 of 5 How do ?? and ??= differ?

    Choose one answer.

    Show the answer: Question 3 of 5

    Answer: ?? returns a fallback value; ??= assigns the fallback only when the variable is null

    cache ??= compute() sets cache only when it is null; name ?? 'guest' just yields a value.

  4. Question 4 of 5 You build a query map and want the city entry omitted when city is null. What do you write?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: 'city': ?city

    searchparams.dart wrote 'city': ?city, so the entry vanished and the map printed {q: chai}.

  5. Question 5 of 5 You change the throw-helper's return type from Never to void and keep if (n == null) fail(...). What happens?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: parsePort stops compiling, because n is still int? on the next line

    never.dart declares fail as Never, which is why return n compiles with no ! after the null check. A void helper would need a return, a throw at the call site, or a null check again.

Interview questions

Warm-up (fresher): What does the ! operator do, and why is it called a code smell?

The ! operator takes a nullable value and asserts to the compiler that it is not null, giving back the non-nullable type so you can use it. It checks nothing itself: if the value is null at run time, it throws Null check operator used on a null value, as bang.dart did. It is called a code smell because each ! is a promise the compiler cannot verify, so it moves a guarantee from compile time, where null safety usually catches mistakes, back to run time, where a wrong guess becomes a crash. It is fine in a test, where throwing is the point, or right after a check that the analyzer cannot follow. Everywhere else, prefer ?? for a fallback, an if that promotes the value, or an if-case pattern, so the null is handled rather than asserted away.

The bank adds two questions that carry null safety into real cleanup and design work:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A screen crashes with "Null check operator used on a null value" for some users but not others. The code reads user profile fields with ! throughout, such as user.phone! and user.address!.city!. Walk through how you would find the crash and remove the ! operators without changing behaviour for users whose fields are set.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A teammate used a language whose nullability was only a warning and asks what "sound" buys that is worth the extra ? and ! in the code. Explain what soundness guarantees, one thing it lets the compiler do, and one class of bug it removes, with a concrete example.

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which Dart types can hold null?

    Only nullable types, written with ?, such as String?. Types are non-nullable by default, and Dart has enforced this since Dart 3.

  2. What does "sound" add to Dart's null safety?

    A non-nullable value can never be null while the program runs, so you can trust the type and the compiler can drop checks.

  3. What does the ! operator do, and what happens when it is wrong?

    It asserts that a nullable value is not null and gives back the non-nullable type. When the value is null it throws "Null check operator used on a null value", so a compile-time guarantee becomes a run-time crash.

  4. What can replace a ! in ordinary code?

    A fallback with ??, promotion after a null check, or an if-case. Keep ! for tests, or right after a check that the analyzer cannot follow.

  5. What does each null-aware operator do when the receiver is null?

    ?. and ?[] skip the access and give null, ?? uses the fallback on its right, ??= assigns only if the variable is null, ...? spreads nothing and ?.. skips the cascade.

  6. When do you use ...? and when ?expr in a collection literal?

    ...? splices in a whole list that might be null, and ?expr, a null-aware element since Dart 3.8, adds one element or entry that might be null. Both leave nulls out, so the collection's type stays non-nullable.

  7. How does a throw-helper typed Never help flow analysis?

    Never is the type of an expression that never completes normally, such as a function that always throws. After if (n == null) fail(...), the compiler knows n is not null, so it promotes n to int with no !.

Key takeaways

  • Types are non-nullable by default; only a ? type such as String? admits null, and Dart has enforced this sound guarantee since Dart 3.
  • “Sound” means a non-nullable value can never be null at run time, so the compiler can drop checks and you can trust the type.
  • ! asserts non-null and throws when wrong (Null check operator used on a null value); replace it with ??, promotion or an if-case, and keep it for tests.
  • The null-aware operators ?., ?[], ??, ??=, ...? and ?.. act only when the receiver is non-null, and Dart 3.8 null-aware elements (?expr) drop null entries from a collection.
  • Assigning String? to String is a compile error, and Never, the type of a throw-helper, lets flow analysis promote a value after the helper is called.

Terms in this lesson

Never
The type of an expression that never completes normally, such as a function that always throws or exit(). A call to a Never-returning function lets flow analysis treat the following code as unreachable.
null-assertion operator
The postfix ! operator, which asserts a nullable value is not null and returns the non-nullable type. If the value is null at run time it throws, so each use is an unchecked promise.
nullable type
A type written with a trailing ?, such as String?, whose values may be null. A type without ? is non-nullable and can never hold null under sound null safety.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.