Skip to content
TaeyoungKim.dev

Dart final vs. const: Why You Can Call add() on a final List

AppWritten 3 min readTaeyoungKim
LinkedInX

You mark a list final, yet add() still works. If you learned that a final value “cannot change,” this may seem surprising. In Dart, final prevents reassigning the variable to another object; it does not freeze the elements of a mutable list. A const list cannot be changed this way.

final fixes the variable's assignment

final prevents reassigning a variable but allows changes to the same mutable list. Attempting add() on a const list leaves it unchanged and throws UnsupportedError.

In short, final prevents reassignment, while a const list also prevents element changes. Try both with the same list of work steps. The variable steps continues to refer to the original list, but the list itself can change.

dart
void main() {
  final steps = <String>['draft'];
  steps.add('review');
  print(steps);
}
text
[draft, review]

Trying to reassign steps = <String>['done']; is rejected during static analysis. In contrast, steps.add('review') changes the contents of the existing list and is allowed. That is why seeing final in a code review does not prove that a passed list can never change.

What happens when you call add() on a const list?

Now create a const list with the same initial item. You can write the add call syntactically, but at runtime the unmodifiable list throws an error.

dart
void main() {
  const fixedSteps = <String>['draft'];
  try {
    fixedSteps.add('review');
  } catch (error) {
    print(error.runtimeType);
  }
}
text
UnsupportedError

The example catches the error only for comparison, not to recommend hiding it. If it occurs in an app, check whether the list was created with const and whether this operation should produce a new state. For example, final nextSteps = [...fixedSteps, 'review']; creates a new list while leaving fixedSteps unchanged.

Distinguish runtime values from immutability

Use const for constant expressions whose values can be determined at compile time. A value such as the current time is only known at runtime, so you may assign final startedAt = DateTime.now(); once, but you cannot create it as a const value. That does not mean every object stored in a final variable is immutable.

If an API contract says the receiver must not change a list, marking your local variable final is insufficient. Decide at the API boundary whether to pass the mutable original, a copy, or an unmodifiable view. When several screens share a list, an add() in one place may affect another. Copying on every input can also increase memory use and allocations, so choose based on list size and change frequency.

Key takeaways: reassignment and mutation are different

final prevents variable reassignment but allows add() on a mutable list it references. A const list prevents element changes and throws UnsupportedError on add(). If data must remain unchanged, inspect the mutability of the passed object as well as the variable declaration.

Author

TaeyoungKim

Connecting technical foundations with implementation, verification, and production decisions.

Read next