Hiprup

What is the difference between `?`, `!`, `late`, and `required` keywords in Dart?

Four keywords for working with Dart's null safety.

? — appended to a type to opt in to nullability. String? can be null or a String.

! — force-unwrap. Asserts the value is non-null; throws if it isn't. Almost always a code smell — defeats the type system. Use sparingly.

late — deferred initialization. Variable is non-nullable but assigned after declaration. late final user is idiomatic for 'init once in initState, never again'. LateInitializationError if read before assigned.

required — modifier on named parameters; caller must pass them. Common on widget constructors: Box({required this.size, this.color = Colors.black}).

// `?`  — makes a type nullable
String? maybeName;            // can be null
int? age;                     // can be null or an int

// `!`  — force-unwrap (assert not null, crash if null)
String? name = fetchName();
final len = name!.length;     // CRASHES if name is null at runtime
// USE SPARINGLY — most uses are smells

// `late` — deferred initialization
late final User user;         // promise: I'll init before any use
late String greeting = expensive();  // lazy: computed on first read
// Crash with LateInitializationError if read before assigned.

// `required` — named param MUST be passed
class Box {
  Box({required this.size, this.color = Colors.black});
  final double size;
  final Color color;
}
// Box(color: Colors.red);     // ❌ compile error: 'size' missing
// Box(size: 10);              // ✅

// Combinations seen in real Flutter code:
class Profile extends StatefulWidget {
  const Profile({super.key, required this.userId, this.compact = false});
  final String userId;        // required, non-null
  final bool compact;         // optional with default

  @override
  State<Profile> createState() => _ProfileState();
}

class _ProfileState extends State<Profile> {
  late final ApiClient api;             // init in initState
  String? lastError;                    // nullable

  @override
  void initState() {
    super.initState();
    api = ApiClient(widget.userId);     // assign late
  }

  void retry() {
    final err = lastError!;             // force-unwrap — caller proved non-null
    print('Retry after $err');
  }
}

The first block shows each keyword in isolation; the final Profile widget combines them in the idiomatic way you'd see in production Flutter code. required this.userId forces callers to pass an id; this.compact = false provides a default; late final api is assigned exactly once in initState (immutability + deferred init); String? lastError is nullable for the optional error case.

The lastError! line is the only force-unwrap and should always be questioned — here it's only safe because the caller has already proven it non-null.

Senior signal: '! is almost always a smell' — force-unwrap means you've defeated the type system. Most uses should be replaced with pattern matching, ?? throw, or early-return guards. The other senior detail: late final is the idiomatic way to express 'I'll initialize this in initState and never again' — it gives you immutability and deferred init. Use late (non-final) sparingly — it can mask bugs.

What is the difference between `?`, `!`, `late`, and `required` keywords in Dart? | Hiprup