Dart: Dart Null Safety — Eliminating Null Pointer Exceptions

Last updated: 2026-08-26

Null Safety is Dart's armor — the compiler helps you block 95% of null pointer attacks.

1. What You Will Learn


2. A Developer's Real Story

(1) Pain Point: NullPointerException Is the Most Common Runtime Crash

Bob's DataPipeline suffered from runtime crashes, 60% of which were NullPointerExceptions. An API returned a null order ID, an optional field was missing in a CSV, a configuration option was used without being set — each situation caused the program to crash, affecting an average of 50,000 orders per incident.

(2) The Solution: Sound Null Safety

Dart 3's Sound Null Safety divides types into nullable T? and non-nullable T. The compiler guarantees at compile-time that non-nullable types can never be null.

DART
// Non-nullable: compiler guarantees non-null
String orderId = 'ORD-001';    // Cannot be null
// orderId = null;              // Compile error!

// Nullable: must be checked before use
String? nickname;               // Can be null
int length = nickname?.length ?? 0;  // Safe access
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

(3) Benefits


3. Sound Null Safety Principles

(1) Type System

100%
flowchart TD
  A[Variable Declaration] --> B{Can be null?}
  B -->|Yes| C["T? Nullable type"]
  B -->|No| D["T Non-nullable type"]
  C --> E["Must check before use"]
  E --> F["if (x != null) → promoted to T"]
  C --> G["?? Provide default value"]
  C --> H["!. Force unwrap - RISKY"]
  D --> I["Use directly - SAFE"]
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: Nullable vs Non-nullable Types

DART
void main() {
  // Non-nullable types - cannot hold null
  String name = 'DataPipeline';
  int count = 100;
  double amount = 1500.0;

  // name = null;    // Compile error!
  // count = null;   // Compile error!

  // Nullable types - can hold null
  String? nickname;
  int? maxRetries;
  double? discountRate;

  print(nickname);       // null
  print(maxRetries);     // null
  print(discountRate);   // null

  // Nullable types require checking before use
  // print(nickname.length);  // Compile error! Might be null
  print(nickname?.length);    // null (safe)
  print(nickname?.length ?? 0); // 0 (with default)
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
Type Can be null Check before use Example
T No Not needed String name = 'Bob'
T? Yes Required String? name

4. Null Checks and Type Promotion

(1) Type Promotion

When the compiler confirms that a nullable variable is not null, it automatically promotes it to a non-nullable type.

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: if-check Promotion

DART
void main() {
  String? name = 'Bob';

  // Before check - nullable
  // print(name.length);  // Error!

  // After null check - promoted to non-nullable
  if (name != null) {
    print(name.length);   // OK! name is promoted to String
    print(name.toUpperCase());  // OK!
  }
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: Multiple Null Check Methods

DART
void main() {
  String? city;

  // Method 1: if-null check
  if (city != null) {
    print(city.length);  // Promoted
  }

  // Method 2: null-coalescing operator ??
  String safeCity = city ?? 'Unknown';
  print(safeCity.length);  // Always safe

  // Method 3: null-aware access ?.
  int? length = city?.length;
  print(length);  // null

  // Method 4: late initialization
  late String resolvedCity;
  resolvedCity = city ?? 'Unknown';
  print(resolvedCity.length);  // Safe

  // Method 5: assert in debug mode
  assert(city != null, 'City must not be null');
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
Check Method Syntax Promotion Effect Safety
if-null if (x != null) Promoted to T Highest
?? x ?? default Returns T High
?. x?.method() Returns T? High
! x!.method() Treated as T Low (may crash)

5. The late Keyword

(1) Delayed Initialization

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: Basic Usage of late

DART
class DataPipeline {
  // late - will be initialized later, but guaranteed before use
  late String outputPath;
  late final DateTime startTime;

  void configure(String path) {
    outputPath = path;  // First assignment
  }

  void start() {
    startTime = DateTime.now();  // late final - set once
    print('Pipeline started at $startTime');
  }
}

void main() {
  final pipeline = DataPipeline();
  pipeline.configure('/tmp/reports');
  pipeline.start();

  // Late initialization with initializer
  late final int maxRecords = _loadConfig();
  // maxRecords is computed only on first access
  print('Max records: $maxRecords');
}

int _loadConfig() {
  print('Loading config...');
  return 1000000;
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: Risks of late

DART
class RiskyPipeline {
  late String config;

  void process() {
    // If config not set, throws LateInitializationError
    print(config);  // Potential runtime crash!
  }
}

void main() {
  final pipeline = RiskyPipeline();
  // pipeline.process();  // LateInitializationError!

  // Safe pattern: initialize in constructor
  final safePipeline = SafePipeline('/etc/config.yaml');
  safePipeline.process();  // OK
}

class SafePipeline {
  late String config;

  SafePipeline(String configPath) {
    config = _loadConfig(configPath);
  }

  void process() => print('Config: $config');

  String _loadConfig(String path) => 'Loaded from $path';
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
late Form Initialization Number of Assignments Risk
late T Before use Multiple Medium (crash if accessed before initialization)
late final T Before use 1 Medium
late final T = expr On first access 1 (automatic) Low

6. The ! Operator

(1) Force Unwrapping

! tells the compiler "I'm sure this value is not null," skipping the null check. However, if the value is null, it will throw a TypeError.

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: Using !

DART
void main() {
  String? name = 'Bob';

  // When you KNOW the value is non-null
  print(name!.length);  // 3 - OK because name is 'Bob'

  // DANGEROUS: if null, crashes!
  String? maybeNull;
  // print(maybeNull!.length);  // Runtime TypeError!

  // Safe alternative: use ?? or if-null check
  print(maybeNull?.length ?? 0);  // 0 - safe
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

(2) When to Use !

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: Reasonable Use Cases for !

DART
class Order {
  final String id;
  final double amount;
  Customer? customer;  // Optional relationship

  Order({required this.id, required this.amount});

  // Use ! when business logic guarantees non-null
  String get customerName {
    // This is risky - customer might be null
    // return customer!.name;  // BAD

    // Better: safe access with default
    return customer?.name ?? 'Unknown Customer';
  }
}

class Customer {
  final String name;
  Customer(this.name);
}

void main() {
  final order = Order(id: 'ORD-001', amount: 1500.0);
  print(order.customerName);  // Unknown Customer

  // After setting customer
  order.customer = Customer('Alice');
  print(order.customerName);  // Alice
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
Use Case Recommended Not Recommended
Nullable variables ?. / ?? / if-null !
API return values Null check !
Collection .first .firstOrNull ?? default .first!
After assertion ! (assertion guarantees non-null)

7. Null Safety and Collections

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: Handling Null in Collections

DART
void main() {
  // List with nullable elements
  List<String?> names = ['Alice', null, 'Bob', null, 'Charlie'];

  // Filter out nulls - type promotion works
  final nonNull = names.whereType``<String>``().toList();
  print(nonNull);  // [Alice, Bob, Charlie]

  // Map with nullable values
  Map<String, double?> revenue = {
    'Electronics': 3600.0,
    'Books': null,
    'Clothing': 890.0,
  };

  // Filter entries with non-null values
  final validRevenue = Map.fromEntries(
    revenue.entries.where((e) => e.value != null),
  );
  print(validRevenue);  // {Electronics: 3600.0, Clothing: 890.0}

  // Safe access with default
  final booksRevenue = revenue['Books'] ?? 0;
  print(booksRevenue);  // 0
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

8. Bob's Scenario: Null Safety Design for DataPipeline

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

: Null Safety Design for Optional Configurations

DART
class PipelineConfig {
  // Required fields - non-nullable
  final String appName;
  final String version;

  // Optional fields - nullable with defaults
  final String? outputPath;
  final String? logLevel;
  final double? customTaxRate;
  final int? maxRetries;

  // Computed from optional - non-nullable
  String get effectiveOutputPath => outputPath ?? '/tmp/datapipeline/output';
  String get effectiveLogLevel => logLevel ?? 'info';
  double get effectiveTaxRate => customTaxRate ?? 0.08;
  int get effectiveMaxRetries => maxRetries ?? 3;

  const PipelineConfig({
    required this.appName,
    required this.version,
    this.outputPath,
    this.logLevel,
    this.customTaxRate,
    this.maxRetries,
  });

  String get summary => '''
$appName v$version
  Output: $effectiveOutputPath
  Log:    $effectiveLogLevel
  Tax:    ${(effectiveTaxRate * 100).toStringAsFixed(1)}%
  Retries: $effectiveMaxRetries
''';
}

void main() {
  // Minimal config - only required fields
  final minimal = PipelineConfig(
    appName: 'DataPipeline',
    version: '1.0.0',
  );
  print(minimal.summary);

  // Full config
  final full = PipelineConfig(
    appName: 'DataPipeline',
    version: '2.0.0',
    outputPath: '/data/reports',
    logLevel: 'debug',
    customTaxRate: 0.10,
    maxRetries: 5,
  );
  print(full.summary);
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

9. Complete Example: Null-Safe Order Processing in DataPipeline

DART
// ============================================
// DataPipeline Null-Safe Order Processing
// Complete demonstration of Null Safety patterns
// ============================================

class Customer {
  final String name;
  final String? email;
  final Address? address;

  Customer({required this.name, this.email, this.address});

  String get displayName => email ?? name;
  String get city => address?.city ?? 'Unknown City';
}

class Address {
  final String city;
  final String? state;
  final String country;

  Address({required this.city, this.state, required this.country});

  String get fullRegion => state != null ? '$city, $state' : city;
}

class Order {
  final String id;
  final double amount;
  final String status;
  final Customer? customer;
  final String? discountCode;
  final double? discountPercent;

  Order({
    required this.id,
    required this.amount,
    required this.status,
    this.customer,
    this.discountCode,
    this.discountPercent,
  }) : assert(amount > 0, 'Amount must be positive');

  double get effectiveDiscount => discountPercent ?? 0;

  double get discountedAmount => amount * (1 - effectiveDiscount);

  double get tax => discountedAmount * 0.08;

  double get total => discountedAmount + tax;

  String get customerName => customer?.name ?? 'Guest';

  String get customerCity => customer?.city ?? 'Unknown';

  String get formattedTotal =>
      '\$${total.toStringAsFixed(2)} USD';

  @override
  String toString() =>
      'Order($id, $customerName, ${formattedTotal}, $status)';
}

class OrderProcessor {
  final List``<Order>`` _orders = [];
  final List``<String>`` _warnings = [];

  void addOrder(Order order) {
    _orders.add(order);

    // Null-safe checks with warnings
    if (order.customer == null) {
      _warnings.add('Order ${order.id}: No customer assigned');
    }
    if (order.discountCode != null && order.discountPercent == null) {
      _warnings.add('Order ${order.id}: Discount code without rate');
    }
  }

  double get totalRevenue =>
      _orders.fold(0.0, (sum, o) => sum + o.total);

  List``<Order>`` get ordersWithCustomers =>
      _orders.where((o) => o.customer != null).toList();

  Map<String, double> revenueByCity() {
    final result = <String, double>{};
    for (final order in _orders) {
      final city = order.customerCity;
      result.update(city, (v) => v + order.total, ifAbsent: () => order.total);
    }
    return result;
  }

  void printReport() {
    print('=== DataPipeline Order Report ===');
    print('Orders: ${_orders.length}');
    print('Revenue: \$${totalRevenue.toStringAsFixed(2)} USD');
    print('With customers: ${ordersWithCustomers.length}');

    print('\nRevenue by City:');
    for (final entry in revenueByCity().entries) {
      print('  ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
    }

    if (_warnings.isNotEmpty) {
      print('\nWarnings:');
      for (final w in _warnings) {
        print('  $w');
      }
    }
  }
}

void main() {
  final processor = OrderProcessor();

  processor.addOrder(Order(
    id: 'ORD-001',
    amount: 1500.0,
    status: 'completed',
    customer: Customer(
      name: 'Alice',
      email: 'alice@example.com',
      address: Address(city: 'New York', state: 'NY', country: 'US'),
    ),
    discountPercent: 0.10,
  ));

  processor.addOrder(Order(
    id: 'ORD-002',
    amount: 3200.0,
    status: 'completed',
    // No customer - nullable field
  ));

  processor.addOrder(Order(
    id: 'ORD-003',
    amount: 890.0,
    status: 'pending',
    customer: Customer(
      name: 'Bob',
      address: Address(city: 'London', country: 'UK'),
    ),
    discountCode: 'SAVE20',
    // discountPercent is null - warning!
  ));

  processor.printReport();
}
TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in this Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.

Output:

TEXT 📖 Display only
=== DataPipeline Order Report ===
Orders: 3
Revenue: $5172.36 USD
With customers: 2

Revenue by City:
  New York: $1458.00 USD
  Unknown: $3456.00 USD
  London: $961.20 USD

Warnings:
  Order ORD-002: No customer assigned
  Order ORD-003: Discount code without rate

❓ FAQ

Q: What's the difference between Sound Null Safety and non-Sound? A: Sound Null Safety guarantees that non-nullable types can never be null throughout the entire program (the compiler performs global verification). Non-Sound mode (deprecated) allows certain paths to bypass checks. Dart 3 defaults to Sound.

Q: When should I use late instead of a nullable type? A: Use late when you are certain the variable will be initialized before use and you don't want it to be null. If the variable truly can be null (semantically optional), use T?.

Q: Should ! be completely avoided? A: Not completely, but use it sparingly. Reasonable scenarios include after assertions, in test code, or when business logic explicitly guarantees non-nullity. In most cases, ?? or ?. is safer.

Q: Is null check promotion effective inside closures? A: Not always. If the variable might be modified outside the closure, the compiler cannot guarantee promotion. Capture it in a local final variable to solve this.

Q: Are generic type parameters nullable by default? A: No. T is non-nullable by default, and T? is nullable. This ensures generic type safety. T? is actually a supertype of T.

Q: How to handle many nullable fields in JSON parsing? A: It's recommended to use ?? to provide default values, or use the json_serializable package to automatically generate a fromJson method with default values. Avoid using ! everywhere.

Q: What's the difference between late final and final? A: final must be assigned at declaration or in the constructor's initializer list. late final can be first assigned in the constructor body or a subsequent method. late final delays the timing of the assignment.


📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Declare 5 variables (2 non-nullable, 3 nullable). Use if-null checks, ??, and ?. to safely access the nullable variables in three different ways, and print the results.
  2. Intermediate (Difficulty ⭐⭐): Design a Config class using late to delay the initialization of 3 fields, set them in a configure() method. Intentionally access the fields without calling configure() and observe the LateInitializationError.
  3. Challenge (Difficulty ⭐⭐⭐): Implement a NullSafe API client where all network requests return a Result<T?> type. Correctly handle three cases: the server returns null, a field is missing, and a type mismatch, all without using !.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏