Course outline

Dart / Foundations: values, absence, collections, and valid objects / Lesson 3

Handle absence without hiding invalid input

Use nullable types, promotion, null-aware operators, and tryParse while keeping late initialization and assertions explicit.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish null, empty text, failed parsing, and a violated domain rule.
  • Explain what ?, !, ??, and late promise at their respective boundaries.

Put optionality into the type

String excludes null under sound null safety, while String? allows a string or absence. An empty string is still a present string with length zero. Decide which meaning the domain needs before choosing a default. A missing optional note can be normal, but a missing required batch identifier may be an integration failure. Null safety helps the analyzer reject many unsafe operations; it does not validate that a present value follows a business format, and explicit assertions or delayed initialization can still cause runtime failures. Keep those distinct guarantees visible.

A null-aware access preserves absence

The ?. operator accesses a member only when its receiver is non-null. The ?? operator chooses its right operand only when the left value is null, not when it is empty or zero. The ! operator instead asserts that a nullable value is present and can fail if that promise is false. It should not be a routine way to silence analysis at an external-input boundary. The example contrasts missing and empty values with a safe fallback. Its output is reasoned from the fixed data rather than captured from a Dart execution.

dart Missing and empty values follow different paths
void main() {
  final String? missing = null;
  final String? empty = '';
  print(missing?.length ?? -1);
  print(empty?.length ?? -1);
  print(missing ?? 'no note');
}
Expected output
-1
0
no note

Expected output is -1, 0, no note. The empty string is not null, so its zero length is retained. No runtime or analyzer was executed.

Let control flow establish a usable local type

After a suitable check, flow analysis can promote a value to a more specific type. An early return for null can make later use of a stable local value straightforward. Promotion depends on the language rules and whether the value can change; not every property or captured mutable variable can be treated like an unchanged local. Use a deliberate local snapshot when that is the intended meaning, rather than inserting ! everywhere. A snapshot is itself a semantic choice: if the business requires the current live property instead, reading it once may change the policy.

Parsing success does not prove domain validity

int.tryParse returns an int or null instead of throwing for ordinary parse failure. A parsed negative integer can still be invalid as a sample count. The helper below checks the positive-count rule after conversion and uses null for both rejection reasons. That is appropriate only when callers do not need detailed causes. A later typed outcome can distinguish malformed text from an unacceptable value. Trimming is an explicit input policy rather than a universal correction; do not apply the same normalization blindly to identifiers or signed protocol data.

dart Parse first, then validate the permitted range
int? positiveCount(String text) {
  final value = int.tryParse(text.trim());
  if (value == null || value <= 0) return null;
  return value;
}
void main() {
  for (final text in <String>[' 3 ', 'bad', '0']) {
    print(positiveCount(text) ?? 'invalid');
  }
}
Expected output
3
invalid
invalid

Expected output is 3, invalid, invalid. Zero is syntactically valid but rejected by the positive-count rule. All strings are fixed synthetic inputs; the program was not run.

Late initialization moves an obligation to runtime

late can support a value initialized after declaration, and a late initializer can also defer work until first use. It does not establish that every lifecycle path performs initialization before a read. Reading an uninitialized late variable can fail at runtime. A late final binding also has a one-assignment obligation. Prefer constructor-required state when an object cannot be meaningful without the value. When delayed initialization is justified, name the owner and timing, test the premature-read path, and avoid exposing a partially prepared object as though its non-null fields were already ready.

Common errors and fixes

A null assertion fails on an ordinary missing field.

Why it happens
Expected absence was treated as an impossible internal state.
How to fix it
Handle absence explicitly through rejection, fallback, or an optional result.
Prevent it next time
Require a real invariant before adding a not-null assertion.

An empty note is mistaken for a missing note.

Why it happens
The application collapsed distinct meanings without a declared policy.
How to fix it
Check emptiness separately when the domain needs that distinction.
Prevent it next time
Include null, empty, whitespace-only, and ordinary values in boundary examples.

A late field is read before setup completes.

Why it happens
The initialization owner or lifecycle order was implicit.
How to fix it
Require construction-time data or enforce an explicit delayed-initialization protocol.
Prevent it next time
Test every path that can expose the object before it is ready.

Check your understanding

1. Why does value?.length ?? 0 lose information if a report distinguishes absent and empty text?

Show worked solution

Both cases become zero, so the report cannot recover which input occurred. Preserve a nullable length or an explicit state until the presentation layer decides how to show each case. A fallback is correct only when combining those meanings is intentional. Safe syntax alone does not prove that collapsing the data matches the product requirement.

2. Does late String id promise that an object always has a valid identifier?

Show worked solution

No. It defers definite initialization responsibility to runtime and does not validate identifier content. A read before assignment can fail, and an assigned empty string can still violate the domain. Prefer required constructor data when possible, then validate the actual format and ownership at the proper boundary. Delaying assignment is not a substitute for either check.

Key takeaways

  • Null-aware operators preserve type safety only within their stated meaning.
  • Presence, valid content, and lifecycle readiness require separate reasoning.

Official references