Dart Basics: Types, final vs const and Null Safety

Dart lecture 1 from real runs: dart run in 5 minutes, final vs const errors, ~/ division, ? ! ?? and late, dart analyze codes and measured startup.

Series: Dart lecture 1/6 (beginner). For people who have never written Dart, or who started a Flutter app and keep hitting red squiggles about null. After this lecture you will run your first program, choose between var, final and const, and read the null-safety errors from the compiler and the crash messages from the runtime.

Dart is the language Flutter apps are written in. If you are joining a team that ships Flutter apps, almost every crash report you will read in your first month is one of the errors on this page: Null check operator used on a null value, LateInitializationError, or a type error the analyzer already warned about. Everything below was actually run with Dart SDK 3.11.5 (stable) on an Apple Silicon Mac (M1 Pro); file paths in the output are shortened to ~/demo/.

Your first program in 5 minutes

If you have Flutter installed, you already have Dart: the dart command ships inside the Flutter SDK. Check it:

$ dart --version
Dart SDK version: 3.11.5 (stable) (Wed Apr 15 00:36:32 2026 -0700) on "macos_arm64"

Create a file called hello.dart:

void main() {
  print('Hello, Dart!');
}

and run it:

$ dart run hello.dart
Hello, Dart!

That is the whole setup. A few terms you just used:

  • main() is the entry point, the function Dart calls first. void means it returns nothing.
  • print() writes one line to the terminal.
  • Every statement ends with ;. Forgetting it is the most common first error. dart run says Error: Expected ';' after this. and points a caret at the spot; dart analyze reports the same thing as Expected to find ';'. - expected_token.

You do not need a project folder for these lectures. dart create -t console my_app makes a full project with a pubspec.yaml and tests, but a single .dart file runs fine on its own, and that is what we do here.

var, final and const (and the errors they give)

A variable is a name for a value. Dart has three ways to declare one:

void main() {
  var count = 3;          // type inferred: int
  final name = 'Mina';    // set once, at run time
  const pi = 3.14159;     // compile-time constant
  final now = DateTime.now();
  print('${count.runtimeType} ${name.runtimeType} ${pi.runtimeType} ${now.year > 2000}');
}
int String double true

We never wrote int or String, but Dart knows them. This is type inference: the compiler looks at the right-hand side and fixes the type of the variable for good. var count = 3 is exactly the same as int count = 3. It is not "a variable that can hold anything", as you will see in a moment.

Now let us break things on purpose. This file has one mistake per line group:

void main() {
  final name = 'Mina';
  name = 'Jun';

  const now = DateTime.now();

  var count = 3;
  count = 'three';

  Object? b = 'hello';
  print(b.length);

  int half = 7 / 2;
}

dart analyze checks a file without running it. Each line ends with a diagnostic code, which is what you search for when you do not understand a message (warnings about unused variables trimmed):

$ dart analyze errors.dart
  error - errors.dart:3:3 - The final variable 'name' can only be set once. Try making 'name' non-final. - assignment_to_final_local
  error - errors.dart:5:15 - Const variables must be initialized with a constant value. Try changing the initializer to be a constant expression. - const_initialized_with_non_constant_value
  error - errors.dart:5:15 - The constructor being called isn't a const constructor. Try removing 'const' from the constructor invocation. - const_with_non_const
  error - errors.dart:8:11 - A value of type 'String' can't be assigned to a variable of type 'int'. Try changing the type of the variable, or casting the right-hand type to 'int'. - invalid_assignment
  error - errors.dart:11:11 - The getter 'length' isn't defined for the type 'Object'. ... - undefined_getter
  error - errors.dart:13:14 - A value of type 'double' can't be assigned to a variable of type 'int'. ... - invalid_assignment

dart run refuses to start the same file and prints its own wording, with a caret under the problem:

errors.dart:3:3: Error: Can't assign to the final variable 'name'.
  name = 'Jun';
  ^^^^
errors.dart:5:24: Error: Cannot invoke a non-'const' constructor where a const expression is expected.
Try using a constructor or factory that is 'const'.
  const now = DateTime.now();
                       ^^^

What each one teaches:

  • final means "assigned once". The value can be computed at run time (DateTime.now() is fine).
  • const means "known when the program is compiled". The current time is not, hence const_initialized_with_non_constant_value.
  • var fixed the type to int on the first line, so count = 'three' is invalid_assignment.
Decision diagram. Question one: will the variable be reassigned later? If yes, use var, or an explicit type such as int. If no, question two: is the value known when the program is compiled, like a literal number or string? If yes, use const. If no, for example DateTime.now() or a value read from a file, use final.
var, final or const? Start from const, fall back to final, and use var only when the value really changes.

The habit most Flutter teams follow (some enforce it with the optional lint prefer_final_locals): write final by default, const for literal values and widgets that never change, and var only when you really reassign. A const widget is also built once at compile time and reused, which is why Flutter code is full of const Text(...).

dynamic vs Object?: two ways to say "anything"

Both dynamic and Object? can hold a value of any type. The difference is what the compiler lets you do with it:

void main() {
  dynamic a = 'hello';
  Object? b = 'hello';
  print(a.length);
  if (b is String) print(b.length); // promoted to String
  a = 42;
  print(a.length);
}

dart analyze reports No issues found! for this file, yet it crashes:

5
5
Unhandled exception:
NoSuchMethodError: Class 'int' has no instance getter 'length'.
Receiver: 42
Tried calling: length

With dynamic, the compiler stops checking, so the mistake shows up only when that line runs, possibly on a user's phone. With Object?, b.length is rejected at compile time (undefined_getter above) until you check the type with is. After if (b is String), Dart promotes b to String inside the block. That is called type promotion, and null safety uses the same trick.

You want Use Checked by
A value whose type you know the type (int, String) or final x = … compiler
"Any value", inspected with is before use Object? (or Object if never null) compiler
Raw JSON you will cast immediately dynamic, then convert to a typed class right away nobody until run time

jsonDecode() returns dynamic, which is why Flutter code that reads API responses is where NoSuchMethodError crashes come from. Convert to typed data at the boundary.

Numbers: int, double and ~/

print(7 / 2);       // 3.5
print(7 ~/ 2);      // 3
print(-7 ~/ 2);     // -3
print(7 % 3);       // 1
print(-7 % 3);      // 2
print((-7).remainder(3)); // -1
print(0.1 + 0.2);   // 0.30000000000000004
print(1 / 0);       // Infinity
print(9223372036854775807 + 1);
print(2.5.round());   // 3
print((-2.5).round()); // -3
3.5
3
-3
1
2
-1
0.30000000000000004
Infinity
-9223372036854775808
3
-3

Four things to remember:

  1. / always returns a double, even for 8 / 2 (it gives 4.0). For integer division use ~/, which truncates toward zero, so -7 ~/ 2 is -3, not -4.
  2. % is never negative for a positive divisor: -7 % 3 is 2. If you want the C/Java style -1, call .remainder().
  3. int silently overflows on the Dart VM and in native apps: the largest 64-bit value plus one wrapped to the smallest. No exception.
  4. round() rounds halves away from zero (2.5 β†’ 3, -2.5 β†’ -3), unlike Python's banker's rounding.

int.parse('42') converts text to a number and throws on bad input; int.tryParse('4x2') returns null instead, which brings us to null safety.

Strings and interpolation

final name = 'Mina';
final apps = 3;
print('Hi $name, you have $apps apps.');
print('Next year: ${apps + 1} apps, ${name.toUpperCase()}');
print('Price: \$${(1234.5).toStringAsFixed(2)}');
print(r'raw \n $name');
print('πŸ˜€'.length);
print('πŸ˜€'.runes.length);
Hi Mina, you have 3 apps.
Next year: 4 apps, MINA
Price: $1234.50
raw \n $name
2
1

$name inserts a variable; ${…} inserts any expression. A literal dollar sign needs \$. Single and double quotes are the same; ''' makes a multi-line string; an r prefix turns off escapes and interpolation. 'Age: ' + apps is a compile error (int is not a String); interpolation is the idiomatic fix.

The emoji line is a real trap: .length counts UTF-16 code units, so one emoji is 2. If you limit a nickname to 10 "characters" with .length, emoji users get 5. .runes.length counts code points; for what a person sees as one character (flags, skin tones) you need the characters package, which Flutter already depends on.

Sound null safety: ?, ?., ??, ! and late

Since Dart 2.12, and with no opt-out since Dart 3, a type like String cannot hold null. If a value may be missing, you add ?: String?. The compiler then makes you deal with the null before you use it.

String? findNickname(String user) => user == 'mina' ? 'Mimi' : null;

void main() {
  String? a = findNickname('mina');
  String? b = findNickname('jun');

  print(a?.length);            // 4
  print(b?.length);            // null
  print(b ?? 'no nickname');   // fallback
  b ??= 'guest';               // assign only if null
  print(b);

  String? c = findNickname('jun');
  if (c != null) {
    print(c.length);           // promoted to String inside the if
  }

  print(c!.length);            // you promised it is not null... it is
}
4
null
no nickname
guest
Unhandled exception:
Null check operator used on a null value
#0      main (file:///~/demo/nulls.dart:18:10)
  • ?. calls the member only if the value is not null; otherwise the whole expression is null.
  • ?? gives a fallback; ??= assigns only when the variable is null.
  • if (c != null) promotes c to String inside the block, exactly like is String did.
  • ! (the null assertion operator) tells the compiler "this is not null, trust me". The compiler stops complaining, and the check moves to run time. When you are wrong, you get the most common crash in Flutter apps.
Flow diagram. A value of type String? can be handled four ways. if (s != null) promotes it to String inside the block. s?.length gives null instead of crashing when s is null. s ?? 'default' replaces null with a fallback. s! claims it is not null; if it is null, the program stops with the runtime error Null check operator used on a null value.
Four ways to handle a String? value. Only ! can crash at run time; the other three are checked by the compiler.

Without the ?, the compiler catches you first:

error - errors.dart:16:14 - The property 'length' can't be unconditionally accessed because the receiver can be 'null'. Try making the access conditional (using '?.') or adding a null check to the target ('!'). - unchecked_use_of_nullable_value
error - errors.dart:18:13 - A value of type 'Null' can't be assigned to a variable of type 'int'. ... - invalid_assignment

Note the analyzer's suggestion: "or adding a null check to the target ('!')". It is technically correct and usually the wrong fix. Reach for if, ?. or ?? first.

late: "I will set it before I read it"

late lets a non-nullable variable start without a value. It is common in Flutter State classes (late final AnimationController controller; set in initState).

class Settings {
  late String token;                         // "I will set it before reading it"
  late final String theme = loadTheme();     // lazy: runs on first read
  late final int id;                         // set once, later

  void login() => token = 'abc123';
}

String loadTheme() {
  print('  loadTheme() runs now');
  return 'dark';
}

void main() {
  final s = Settings();
  print('created Settings');
  print(s.theme);
  print(s.theme);

  s.id = 1;
  try {
    s.id = 2;
  } catch (e) {
    print(e);
  }
  print(s.token);                            // forgot to call login()
}
created Settings
  loadTheme() runs now
dark
dark
LateInitializationError: Field 'id' has already been initialized.
Unhandled exception:
LateInitializationError: Field 'token' has not been initialized.
#0      Settings.token (~/demo/late.dart)
#1      main (~/demo/late.dart:26:11)

Three behaviours in one run: late final … = loadTheme() ran only on first read and only once; a late final set twice throws; reading an unset late field throws. For local variables the analyzer can see the mistake: late String msg; print(msg); gives definitely_unassigned_late_local_variable. For fields it cannot, so late is a promise checked only at run time, like !.

Common mistake: silencing the analyzer with !

The situation. A screen shows a user's nickname. The model has String? nickname, the widget does Text(user.nickname!), the red squiggle disappears, and the app works in testing because every test account has a nickname.

What actually happens. The first real user without a nickname opens the screen and gets Null check operator used on a null value. In a Flutter debug build that is a red error screen; in release it is a grey box or a crash report. The analyzer had warned you; ! turned the warning off.

The fix. Decide what the screen should show when the value is missing, and write it down in code:

Text(user.nickname ?? 'Guest')

or, if a missing value means "show nothing", if (user.nickname != null) Text(user.nickname!) is fine only because the check is right next to the !. A quick way to audit an inherited codebase: search for !. and !) and ask, for each one, "who guarantees this?"

A measurement: dart run vs a compiled exe

dart run compiles your code just in time (JIT) every time it starts. dart compile exe compiles it ahead of time (AOT) into a native executable, the same kind of compilation a Flutter release build uses.

$ dart compile exe hello.dart -o hello
Generated: ~/demo/hello

Compiling took 0.87 s and the executable is 5,587,776 bytes (about 5.3 MB, because it carries the Dart runtime). We timed hello.dart 20 times per command and took the median, then repeated that 3 times:

Command Median start-up (3 batches)
dart run hello.dart 330–340 ms
dart hello.dart 193–210 ms
./hello (AOT exe) 16–18 ms
Bar chart of start-up times for the same hello-world file. dart run hello.dart took 334 milliseconds, dart hello.dart took 199 milliseconds, and the executable built by dart compile exe took 17 milliseconds.
Start-up time of a hello-world program (median of 20 runs, mean of 3 batches). Dart 3.11.5, Apple M1 Pro. The compiled exe starts about 12 times faster than dart file.dart.

Two findings a summary will not tell you. First, dart run file.dart was consistently about 130 ms slower than dart file.dart for the same file; dart run goes through the package-resolution path (it is the same command that runs bin/ scripts in a project), while dart file.dart just loads the file. We did not dig deeper than that. For quick experiments, dart file.dart is enough. Second, the crash message from the AOT build was the same Null check operator used on a null value, but the stack trace lost its column numbers (nulls.dart:18 instead of nulls.dart:18:10), and both builds exited with code 255.

Honest limits / version notes

  • Everything was run on Dart 3.11.5 only, on an Apple M1 Pro. Timings will differ on other machines; the ratios are what matters.
  • Null safety arrived in Dart 2.12. We checked the "no opt-out" claim directly: a file starting with // @dart=2.9 fails on 3.11.5 with illegal_language_version_override ("The language version must be >=2.12.0").
  • Integer overflow wrapping applies to the Dart VM and native (Flutter mobile/desktop). On the web, int is a JavaScript number with different limits. We did not run a web build for this post.
  • dart analyze here ran without a pubspec.yaml, so only the default diagnostics appear. A project from dart create or flutter create adds a recommended lint package on top, which needs pub get; we kept to single files with no packages.

Exercises

1. Without running it, predict the output:

print(17 ~/ 5);
print(17 % 5);
print(-17 ~/ 5);
print(-17 % 5);
print(17 / 5);

2. Write int nameLength(String? name) that returns the length, or 0 when name is null, without using ! or if.

3. This code crashes. Explain the error message and fix it two ways.

String? title;
print(title!.toUpperCase());

Answers

1. Real output: 3, 2, -3, 3, 3.4. ~/ truncates toward zero (-3, not -4), and % with a positive divisor is never negative, so -17 % 5 is 3 (because -17 = -4 Γ— 5 + 3).

2. int nameLength(String? name) => name?.length ?? 0; printed 4 for 'Mina' and 0 for null. ?. makes the result int?, and ?? turns it back into int.

3. Null check operator used on a null value: ! claimed title was not null, but it was. Fix with a fallback, print(title?.toUpperCase() ?? '(untitled)');, or with a check, if (title == null) return '(untitled)'; followed by title.toUpperCase() (promoted, no !). Bonus: in our run the analyzer flagged the first fix in that exact snippet with a dead_code warning, because flow analysis could prove title is always null there. In real code the value comes from somewhere else, and the warning disappears.

Next

The next lecture covers functions (named and required parameters, closures) and collections, including the lazy Iterable trap and the errors you get when you change a list while looping over it.

FAQ

What does "Null check operator used on a null value" mean in Dart or Flutter?

Some code used the ! operator on a variable that was null at that moment. ! tells the compiler "trust me, this is not null"; the check moves to run time and the program stops. Replace ! with a null check (if (x != null)), ?. or a ?? fallback.

What is the difference between final and const in Dart?

Both can be assigned only once. const must be known when the program is compiled (literals, other consts), while final can be computed at run time. const now = DateTime.now() fails with const_initialized_with_non_constant_value; final now = DateTime.now() is fine.

Why does Dart say 7 / 2 can't be assigned to an int?

In Dart, / always returns a double, so int half = 7 / 2 is a compile error (invalid_assignment). Use the integer division operator ~/ (7 ~/ 2 is 3) or convert explicitly with (7 / 2).toInt() or .round().

  • #dart
  • #beginner
  • #null-safety
  • #types
  • #flutter
  • #dart-analyze

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run β€” and the code that fixed them.

About Β· Apps

↑