In Dart, a Cart constructor declares {this.items = const []}; why does cart.items.add(item) work for some carts and throw UnsupportedError for others?
answer
- defaults must be constants
- const [] rejects writes
- caller-supplied lists are growable
- all default carts share one list
- copy into a fresh list
basics
~20 sA parameter default must be a compile-time constant, so carts built without items get the unmodifiable, canonicalized const [] and add throws; carts given a list share the caller's growable list and add succeeds. Copy the argument into a fresh list instead.
solid answer
~40 sDefault values of optional parameters must be constant, so `this.items = const []` stores a `const` list whenever the caller omits `items`. That object is unmodifiable, so `cart.items.add(...)` throws `UnsupportedError` at runtime, yet it compiles because the static type is `List<CartItem>`. Carts built with `Cart(items: myList)` store the caller's growable list, so the same call works there, and also mutates the caller's list, since the two are aliased. The bug is therefore data-dependent and often slips past tests that always pass a list. Fix it by owning the list: `Cart({List<CartItem> items = const []}) : items = List.of(items);`, or `Cart({List<CartItem>? items}) : items = [...?items];`. Or embrace immutability and replace the list on each change.
code
dart · 20 linesclass CartItem {
const CartItem(this.name);
final String name;
}
class Cart {
Cart({List<CartItem> items = const []}) : items = List.of(items);
final List<CartItem> items;
void add(CartItem item) => items.add(item);
}
void main() {
final a = Cart()..add(const CartItem('milk'));
final shared = [const CartItem('tea')];
final b = Cart(items: shared)..add(const CartItem('milk'));
print(a.items.length); // 1
print(shared.length); // 1: the caller's list was copied, not aliased
print(b.items.length); // 2
}go deeper
Recall that parameter defaults must be constant, and that const [] cannot be added to.
Explain why the failure depends on the construction path, including canonicalization of const [] and aliasing of caller lists.
Diagnose data-dependent UnsupportedError reports, fix constructors with copies or immutable designs, and add tests for both construction paths.
Choose between mutable models that copy on construction and immutable ones that replace lists, and make that choice consistent across the codebase.
## The code that fails only sometimes ```dart class Cart { Cart({this.items = const []}); final List<CartItem> items; void add(CartItem item) => items.add(item); } Cart(items: [CartItem('tea')]).add(CartItem('milk')); // works Cart().add(CartItem('milk')); // UnsupportedError ``` The code compiles, the analyzer is silent, and the behaviour depends on how the object was **constructed**. That combination is what makes it a production bug rather than a typo. ## Why the default is unmodifiable Dart requires the default value of an optional parameter to be a **compile-time constant**. A plain `[]` is not constant, so the only list literal you can write there is `const []`. Constant collections are unmodifiable: - `add`, `remove`, `[]=`, `sort` and `clear` all throw `UnsupportedError`; - the static type is still `List<CartItem>`, so nothing flags the call before it runs. Constants are also **canonicalized**: every `const []` of the same type is the same object. So all carts created without items share one list; if it were mutable, they would all share their contents too. The fact that it throws is the language protecting you from that. ## Why the other path works, and is also wrong When the caller passes a list, the field stores **the caller's own list**: - it is growable, so `add` works; - it is **aliased**: `cart.add(...)` also appends to the list the caller still holds, and the caller's later changes show up in the cart. So one construction path throws and the other silently shares state. Both are symptoms of the same design flaw: the class never took ownership of its list. ## Fixes 1. **Copy into a growable list you own**: keep the constant default but copy in the initializer list. ```dart Cart({List<CartItem> items = const []}) : items = List.of(items); ``` 2. **Nullable parameter plus a spread**: `Cart({List<CartItem>? items}) : items = [...?items];` produces a fresh growable list in both cases. 3. **Make the cart immutable**: keep `const []` as a perfectly good default, expose the list read-only, and implement `add` as returning a new `Cart` with `[...items, item]`. This is the usual shape for state objects handed to Flutter state-management code. ## Where `const []` defaults are fine The pattern is not wrong in itself. It is correct wherever the list is **only read**: - Flutter widget constructors often default list parameters to `const []`; for example, `Column` inherits a `children` default of `const <Widget>[]` from `MultiChildRenderObjectWidget`. Widgets are immutable descriptions and never add to their children. - Value objects and immutable state classes that only read their lists. The trap appears only when a class that **mutates** its list takes that list from a constructor parameter. ## The same trap with maps and sets Nothing here is special to lists. Any constant collection default behaves the same way: - `Settings({this.flags = const {}})` stores a constant `Map` or `Set`, depending on the declared type; `flags['beta'] = true` or `flags.add('beta')` throws for default instances; - `putIfAbsent`, `update`, `remove` and `clear` throw too; - the fix is identical: copy with `Map.of(flags)` or `Set.of(flags)` in the initializer list, or keep the object immutable. Review any class whose constructor takes a collection with a `const` default and whose methods mutate that field. ## How to catch it | Signal | What to check | |---|---| | `UnsupportedError: Cannot add to an unmodifiable list` or similar in a report | constructors with `const` collection defaults | | Tests pass, production fails | tests that always pass an explicit list | | Two objects "sharing" items | constructors that store caller lists without copying | Add a test that constructs with **defaults** and then mutates, and one that mutates the original argument after construction and asserts the object did not change.
- Why can't a Dart optional parameter default to a plain [] instead of const []?Default values must be compile-time constants, and a non-const list literal creates a new object at runtime. The language therefore only accepts a `const` collection there. The usual workaround for a mutable default is a nullable parameter with a copy in the initializer, such as `items = [...?items]`.
- Is the const [] default a problem in a Flutter widget constructor?Usually not. Widgets are immutable configuration and only read their list parameters, so a shared unmodifiable default is exactly right; `Column` inherits a `children` default of `const <Widget>[]`. It becomes a bug only in a class that mutates the list it received.
saying these in an interview costs you the question
- A const [] default gives each object its own empty list
- The analyzer reports add() on a const default list as an error
- Carts built with an explicit list are safe because that list is growable
- const [] defaults are always wrong, even in widget constructors
- Marking the items field final prevents the UnsupportedError