Ankit Ranjan
Back to Deep Dives

Variables and Keywords — var, final, const, and Friends

How Dart stores and protects data. Understanding var, final, const, late, and dynamic unlocks safe, predictable code.

May 24, 2025 8 topics 7 quiz questions
Share:
1

Variables are labelled boxes

A variable is a name attached to a value. Think of it as a labelled box: the label is the variable name, the contents are the value. When we write var name = 'Dart';, we're creating a box labelled "name" and putting the string 'Dart' inside. In Dart, every variable has a type. The type describes what kind of values the box can hold. A String variable can only hold strings. An int variable can only hold integers. Try putting a string into an int box, and the compiler will refuse.

// Creating variables
var name = 'Dart';      // Type inferred as String
String language = 'Dart'; // Type explicitly stated
int count = 42;         // Integer variable
double pi = 3.14159;    // Floating-point variable
bool isReady = true;    // Boolean variable
The key question is: what can we do with this box once it's created? Can we replace the contents? Can we change them after the program is compiled? This is where keywords like var, final, and const come in.

2

var — the mutable default

The var keyword creates a variable whose value can be changed. It's the most flexible option. Declare it once, reassign it as many times as you want.

var score = 0;
score = 10;      // OK
score = 100;     // OK
score = 'high';  // ERROR: can't change type
Notice the last line. With var, we can change the value, but not the type. Once Dart infers that score is an int, it stays an int forever. This is static typing at work — the type is fixed at compile time, even though the value isn't. When should we use var? When we genuinely need to reassign the variable. Loop counters, accumulators, state that changes over time. But if we don't need to reassign, there's a better option.

3

final — set once, keep forever

A final variable can only be assigned once. After that first assignment, the reference is locked.

final name = 'Dart';
name = 'Flutter';  // ERROR: can't reassign final variable
The value of final is computed at runtime. This means we can assign the result of a function call, user input, or any expression that evaluates when the program runs.
final now = DateTime.now();     // Computed at runtime
final user = fetchCurrentUser(); // From a function
final config = loadConfig();    // Loaded from file
Using final tells other developers (and ourselves): "This value won't change after it's set." It makes code easier to reason about. We don't have to track whether the variable might have been reassigned somewhere else. The Dart style guide recommends final over var when reassignment isn't needed. It's a good default.

4

const — known at compile time

A const variable is a compile-time constant. Its value must be determinable before the program even runs. The compiler bakes the value directly into the program. var vs final vs constvarvar name = 'Dart';name = 'Flutter';Can reassign?✓ YesType inferred?✓ YesWhen resolved?runtimeMutable referencefinalfinal name = 'Dart';name = 'Flutter';Can reassign?✗ NoType inferred?✓ YesWhen resolved?runtimeImmutable referenceconstconst pi = 3.14159;pi = 3.14;Can reassign?✗ NoType inferred?✓ YesWhen resolved?compile-timeCompile-time constant

const pi = 3.14159;           // OK: literal value
const doubled = pi * 2;       // OK: computed from const
const now = DateTime.now();   // ERROR: not compile-time
The third line fails because DateTime.now() returns a different value each time the program runs. The compiler can't know what "now" will be. const is powerful because identical const values share memory. Create the same const list in two places, and Dart stores only one copy. This matters for performance and equality checks.

5

Type inference — let the compiler work

Dart is statically typed, meaning every variable has a fixed type. But we don't always have to write that type explicitly. The compiler can often figure it out from context. This is type inference. Type Inference: The Compiler Figures It OutExplicit TypeStringname = 'Dart';intcount = 42;List<int>nums = [1, 2, 3];=Inferred Typevar name = 'Dart';→ Stringvar count = 42;→ intvar nums = [1, 2, 3];→ List<int>Both are statically typed. The compiler infers the type from the right-hand side. When should we use explicit types? The Dart style guide says: prefer var for local variables when the type is obvious. Use explicit types for public APIs (function parameters, return types, class fields) where clarity matters more.

// Local variables: var is fine
var items = fetchItems();

// Public API: explicit types are clearer
List<Item> fetchItems() { ... }
void addItem(Item item) { ... }

6

late — promise to initialise later

The late keyword tells Dart: "I'm not assigning this now, but I promise it will be assigned before anyone reads it." late — Promise to Initialise LaterDeclarationlate String name;No value yet⚠ Access here = LateInitializationErrorAssignmentname = fetchName();Value assigned✓ Safe to accessCommon Use Cases• Dependency injection — assign after constructor• Expensive computation — lazy initialisation with late final• Circular references — A needs B, B needs A• Test setup — assign in setUp(), use in tests If we break the promise and read before assignment, Dart throws a LateInitializationError at runtime. It's a contract: we take responsibility for ensuring initialisation happens.

class UserService {
  late final Database _db;

  void init(Database db) {
    _db = db;  // Assigned after constructor
  }

  User getUser(int id) => _db.query('...');
}
The late final combination is powerful: lazy initialisation that can only happen once. The expensive computation runs only when first accessed.

7

dynamic — the escape hatch

Sometimes we genuinely don't know the type. JSON from a server, values from reflection, interop with JavaScript. For these cases, Dart offers dynamic — a way to opt out of static type checking. dynamic vs Object — Escaping the Type Systemdynamicdynamic value = 'hello';value = 42;// OKvalue = true;// OKvalue.foo();// Compiles! Crashes at runtime⚠ No compile-time checksObjectObject value = 'hello';value = 42;// OKvalue = true;// OKvalue.foo();// Compile error!✓ Only Object methods allowed With dynamic, any method call compiles — but might crash at runtime. The alternative is Object (or Object?), which accepts any value but only allows methods that exist on all objects.

dynamic d = 'hello';
print(d.length);    // OK at compile and runtime
print(d.foo());     // Compiles, crashes at runtime

Object o = 'hello';
print(o.length);    // Compile error: Object has no length
Use dynamic sparingly. Every use is a place where the compiler can't help us catch mistakes.

8

Practical guidelines

So what should we actually use? Here's a decision tree:

Will the value change? No → use final
Is it a compile-time constant? Yes → use const
Do you need to reassign? Yes → use var
Can't initialise in constructor? Consider late
Don't know the type at all? Use Object first, dynamic as last resort

// Good defaults
final name = 'Dart';           // Won't change
const pi = 3.14159;            // Compile-time constant
var count = 0;                 // Needs to change
late final config = loadConfig(); // Lazy + immutable
The goal is to give the compiler (and future readers) as much information as possible. final says "this won't change." const says "this can't change, ever, and is known at compile time." These constraints help us write safer code. As a rule of thumb: default to final, reach for const when possible, use var only when reassignment is genuinely needed.

Variables and Keywords Quiz

7 questions

Test your understanding of Dart's variable declaration keywords.

Search

Loading search...