ZetCode

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.

const_vs_final.dart
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.

const_constructor.dart
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.

canonicalisation.dart
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.

const_collections.dart
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.

deep_immutability.dart
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.

const_contexts.dart
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.