Dart const and Immutability
last modified October 18, 2026
In this article we explore const and immutability in Dart, two related ideas that together let the compiler fold values at build time and let you model data that cannot change after creation.
const and Immutability Overview
Dart has two keywords for values that do not change, and they are easy to
confuse. final means "assigned once, at runtime". A
final variable can hold the result of a function call, a
DateTime, or any other value computed while the program runs, but it
cannot be reassigned. const is stronger: it means "known at
compile time". A const value must be computable before the program
starts, so it can be embedded directly in the compiled output.
Immutability is the broader goal: an object's observable state cannot change after
construction. Declaring instance fields final prevents reassignment,
but any objects they reference must also be immutable for deep immutability.
Instance fields cannot be declared const. A
const constructor allows constant invocations, which enable
canonicalisation and use in const collections and constant patterns.
| Keyword | Assigned when | Can hold runtime value | Example |
|---|---|---|---|
var |
Any time | Yes | var n = 1; |
final |
Once, at runtime | Yes | final now = DateTime.now(); |
const |
At compile time | No | const pi = 3.14; |
const constructor |
At compile time in a const context; otherwise at runtime | Yes, in a non-const invocation | final p = Point(x, y); |
late final |
Once, on first access | Yes | late final id = _nextId(); |
const versus final
Because const values are known at compile time, they can take part
in other constant expressions. A final variable cannot, because its
value is only known at runtime.
void main() {
const int a = 3; // compile-time constant
final int b = 3; // runtime constant, assigned once
const int sum = a + 5; // OK: both operands are constant
print(sum);
// const int bad = a + b; // error: b is not a compile-time constant
print(a + b);
final now = DateTime.now(); // final allows a runtime value
print(now.year > 0);
}
The expression a + 5 is a compile-time constant because both
operands are const, so sum is allowed. The commented
line would fail because b is final: the compiler cannot
use it as a constant expression. Unlike const, both
final and var can hold DateTime.now(), whose
value is not known until the program runs.
$ dart run const_vs_final.dart 8 6 true
const Constructors
A const constructor lets a class create compile-time constant
instances. All instance fields must be final, and constructor
initializers must support compile-time evaluation with constant arguments.
Invoking the constructor in a const context requires constant arguments and
creates a constant object. Outside a const context, the same constructor can
accept runtime arguments and create a non-constant object.
class Point {
final double x, y;
const Point(this.x, this.y);
double distanceSquaredTo(Point other) {
final dx = x - other.x;
final dy = y - other.y;
return dx * dx + dy * dy;
}
@override
String toString() => 'Point($x, $y)';
}
void main() {
const a = Point(0, 0);
const b = Point(3, 4);
print(a);
print(b.distanceSquaredTo(a));
}
All fields of Point are final, so a and
b can be created as constants. The class also carries normal
behaviour: distanceSquaredTo computes a value from the immutable
coordinates. An immutable value type that also has methods is a common and
idiomatic pattern in Dart.
$ dart run const_constructor.dart Point(0.0, 0.0) 25.0
Canonicalisation and identical
The Dart compiler canonicalises constant expressions: two
const objects with identical fields become the very same instance.
A non-constant object created with the same arguments is a separate allocation.
class Point {
final int x, y;
const Point(this.x, this.y);
}
void main() {
const a = Point(1, 2);
const b = Point(1, 2);
final c = Point(1, 2);
print(identical(a, b)); // true: canonicalised to one instance
print(identical(a, c)); // false: a fresh runtime instance
print(a == b); // true
print(a == c); // false
}
Because a and b are both const and equal,
identical(a, b) is true — they share storage. The
final variable c is built at runtime, so it is a
distinct object and identical(a, c) is false. The
default == is identity based, which is why the four results split
the way they do.
$ dart run canonicalisation.dart true false true false
const Collections
Collection literals can be const as well. A constant collection is
completely unmodifiable, and it can itself be stored in a const
variable. A regular final collection is a different matter: the
variable cannot be reassigned, but the collection can still be mutated.
void main() {
const list = [1, 2, 3];
const map = {'a': 1, 'b': 2};
const set = {1, 2, 3};
print(list);
print(map);
print(set);
final mutable = [1, 2, 3];
mutable.add(4);
print(mutable);
var frozen = const [1, 2, 3];
try {
frozen.add(4);
} catch (e) {
print('immutable: ${e.runtimeType}');
}
}
The const list, map, and set are immutable for their whole lifetime.
The final list mutable accepts a new element, showing
that final only prevents reassigning the variable. Attempting to
mutate a const collection throws an UnsupportedError at
runtime.
$ dart run const_collections.dart
[1, 2, 3]
{a: 1, b: 2}
{1, 2, 3}
[1, 2, 3, 4]
immutable: UnsupportedError
Deep Immutability
A final field makes the reference immutable, not the object it
points to. If that object is a mutable collection, the contents can still change
underneath, so the class is only shallowly immutable. Storing a
const collection instead gives a genuinely frozen value.
class Basket {
final List<String> items;
const Basket(this.items);
}
void main() {
final items = ['apple', 'pear'];
final basket = Basket(items);
items.add('plum');
print('shallow: ${basket.items}'); // sees the later mutation
const frozen = ['apple', 'pear'];
final safe = Basket(frozen);
// frozen.add('plum'); // would throw UnsupportedError at runtime
print('frozen: ${safe.items}');
}
The first basket keeps a reference to a mutable list, so adding plum
after construction is visible through the basket's items field. The
second basket is built from a const list, so its contents cannot be
modified at all. When a class is meant to be truly immutable, use
const collections or unmodifiable defensive copies, and ensure their
elements are immutable too.
$ dart run deep_immutability.dart shallow: [apple, pear, plum] frozen: [apple, pear]
const in Patterns and Defaults
Constant patterns match using equality against a compile-time constant. Object patterns instead inspect an object's type and getters at runtime; they do not require a const constructor or a constant instance. Default parameter values must be compile-time constants.
class Point {
final int x, y;
const Point(this.x, this.y);
}
const origin = Point(0, 0);
String describe(Point p) => switch (p) {
origin => 'constant origin',
Point(x: 0, y: 0) => 'origin',
Point(x: var x) when x > 0 => 'right side',
Point() => 'left side',
};
int add({int a = 0, int b = 0}) => a + b;
void main() {
print(describe(origin));
print(describe(Point(0, 0)));
print(describe(Point(1, 2)));
print(describe(Point(-1, 2)));
print(add(a: 2, b: 3));
}
The first case uses the named constant origin as a constant pattern.
Since Point does not override ==, it matches by identity:
the canonical origin matches, but a fresh Point(0, 0) does not. The
next case uses an object pattern to inspect coordinates, so it also matches that
non-constant point. The guard tests x > 0 at runtime. The
add function separately demonstrates constant default values.
$ dart run const_contexts.dart constant origin origin right side left side 5
Source
Dart Language: final and const
Dart Language: Constant constructors
Dart Language: Built-in types
Effective Dart: Prefer const declarations
const and final both prevent reassignment, but only
const produces compile-time values. Const constructors allow constant
instances that can be canonicalised and embedded in const collections. Object
patterns also work with non-constant instances. True immutability requires care
with referenced objects, since a final field can still point at mutable state.
Author
List all Dart tutorials.