skip to content

In Dart, why can you add items to a list held in a final variable, and what changes if the list is const?

level: middleimportance: should knowfreq 58%

answer

  1. binding versus object
  2. final list still grows
  3. const is deeply immutable
  4. add type-checks, then throws
  5. var x = const [] is reassignable

basics

~20 s

final freezes only the variable binding, so the list it references stays mutable and add() works. A const list is a deeply immutable constant: add() still compiles but throws UnsupportedError at runtime, and everything inside a constant is constant too.

solid answer

~50 s

`final` is **shallow**: it stops the variable from being reassigned, but says nothing about the object it points to. `final units = ['m', 'km']; units.add('mi');` is fine, while `units = [];` is a compile error. A `const` value is **deep**: the list and everything in it are compile-time constants, so the object can never change. Because `add` is part of the `List` interface, `const ['m'].add('mi')` still type-checks, but on the VM it throws `UnsupportedError` ("Cannot add to an unmodifiable list") at runtime. The binding and the value are separate choices: `var current = const ['m'];` lets you point `current` at a new list later but never mutate the constant, and `final fixed = const ['m'];` freezes both. Similarly, a class whose fields are all `final` can still expose mutable state if one field holds a mutable `List`.

code

dart · 17 lines
dart
void main() {
  final mutableUnits = ['m', 'km'];
  mutableUnits.add('mi'); // fine: the list itself is mutable
  // mutableUnits = []; // compile error: final binding

  const fixedUnits = ['m', 'km'];
  try {
    fixedUnits.add('mi'); // type-checks, fails at runtime
  } on UnsupportedError catch (e) {
    print(e); // on the VM: Unsupported operation: Cannot add to an unmodifiable list
  }

  var current = const ['m'];
  current = ['m', 'ft']; // allowed: var binding, new growable list
  current.add('in');
  print('$mutableUnits $current'); // [m, km, mi] [m, ft, in]
}

go deeper

for a junior

Remember that final stops reassignment only, so a final list can still grow, while a const list cannot change.

for a middle

Explain references, why mutating a constant compiles yet throws, and how binding and value choices combine.

for a senior

Spot APIs that leak mutable internals through final fields and choose constants or read-only views deliberately.

for a principal

Set an immutability policy for shared library types, weighing constants, read-only views and copying costs.

## Variables hold references In Dart every value is an object and a variable holds a **reference** to it. dart.dev puts it plainly: `var name = 'Bob';` makes `name` contain a reference to a `String` object. That is why there are two separate questions for any variable: 1. Can the **variable** be pointed at a different object? 2. Can the **object** it points to be changed? `final` answers only the first. `const` answers both. ## final is shallow ```dart final units = ['m', 'km']; units.add('mi'); // OK: the list is an ordinary growable list // units = []; // compile error: final variable set twice ``` dart.dev's note on final and const says it directly: although a `final` object cannot be modified, its fields can be changed. More precisely, the *variable* cannot be reassigned, and whatever the object allows, you can still do through it. The same applies to classes. A converter class with `final List<String> supportedUnits;` has a field that cannot be reassigned, yet `converter.supportedUnits.add('league')` changes the converter's state from outside. An all-final class is not an immutable class unless its fields hold immutable objects. ## const is deep A `const` object and its fields cannot be changed; dart.dev calls them **immutable**. Because a constant is built by the compiler from other constants, everything reachable from it is constant as well: the list, the strings in it, and any const objects it holds. The type system does not show this. A constant list's static type is still `List<String>`, and `List` declares `add`, so ```dart const units = ['m', 'km']; units.add('mi'); ``` compiles. At runtime the constant list rejects the call. On the Dart VM, constant lists are an internal immutable list class whose `add` throws `UnsupportedError('Cannot add to an unmodifiable list')`, printed as `Unsupported operation: Cannot add to an unmodifiable list`. ## Binding and value are independent | Declaration | Reassign variable? | Mutate list? | |---|---|---| | `var a = ['m'];` | Yes | Yes | | `final b = ['m'];` | No | Yes | | `var c = const ['m'];` | Yes | No (throws) | | `final d = const ['m'];` | No | No (throws) | | `const e = ['m'];` | No | No (throws) | dart.dev shows the third row explicitly: `foo = [1, 2, 3]; // Was const []` is legal for a `var` that used to hold a constant. ## Common interview traps - Saying a `final` list is read-only. It is not; only the variable is fixed. - Saying the compiler stops `add` on a constant list. It does not; the failure is a runtime `UnsupportedError`. - Forgetting that `var` and `const` values combine: the variable can move to a new list while the constant list itself stays untouched. - Assuming an object is immutable because its class has only `final` fields. Each of these rests on the same confusion between the **binding** (what the variable refers to) and the **object** (what that thing contains). ## Practical consequences - **Defensive APIs**: returning a `final` field's list lets callers mutate your internals. Return a constant, or an unmodifiable view, when the data should be read-only. - **Late failures**: mutating a constant fails at runtime, not compile time, so tests should cover code paths that might try it. - **Configuration tables**: in a unit-conversion library, a `const` table of factors is safe to share everywhere; a `final` table built at runtime is safe from reassignment but not from `add` or `[]=`. - **Annotations**: `package:meta`'s `@immutable` asks the analyzer to warn when a class has non-final fields, but it cannot make a field's list immutable either. ## Boundaries This question is about the depth of `final` versus `const`. Building read-only collections at runtime with unmodifiable views, and const constructors for your own classes, are covered with collections and constructors.

  • Is a Dart class whose fields are all final immutable?
    Not necessarily. final fields cannot be reassigned, but if one holds a mutable `List` or `Map`, callers can still change its contents. The class is only effectively immutable when every field holds an immutable object, such as a constant or an unmodifiable view.
  • Why doesn't the Dart analyzer reject add() on a const list?
    A constant list's static type is plain `List<T>`, and `List` declares `add`, so the call type-checks. Immutability is a property of the runtime object, which throws `UnsupportedError` when mutated. That is why such bugs show up in tests or production rather than at compile time.

final is like a reserved parking space: that space will always hold the same car, but anyone can open the car and rearrange what is inside. const is like a sealed museum exhibit: the car and everything in it are fixed, and trying to open a door gets you stopped at the time you try, not when you walked in.

saying these in an interview costs you the question

  • A final list cannot have items added or removed
  • Calling add on a const list is a compile-time error
  • A var holding a const list can never be reassigned
  • Making every field final guarantees a Dart object is immutable
  • const freezes the outer list but its elements can still change