airbnb/epoxy · error · IllegalStateException
Models cannot be changed once they are added to the…
Error message
Models cannot be changed once they are added to the controller
What it means
ControllerModelList is the list backing an EpoxyController after models have been built and handed off to the adapter; its observer forbids insertions because mutating built models invalidates the diffing/update pipeline. Any call that inserts into the list after buildModels output is finalized throws this IllegalStateException.
Solutions
- Do all model additions inside buildModels() — call requestModelBuild() to rebuild instead of mutating the list.
- Keep model data in separate fields; mutate the data, then trigger requestModelBuild().
- Never hold references to the controller's internal model list outside buildModels().
Example fix
// before
List<EpoxyModel<?>> models = new ControllerModelList(...); // captured
button.setOnClickListener(v -> models.add(newModel())); // throws
// after
button.setOnClickListener(v -> {
items.add(newItem());
controller.requestModelBuild(); // rebuild models in buildModels()
}); Defensive patterns
Strategy: type-guard
Validate before calling
// never mutate ControllerModelList outside buildModels(); mutate your own data list instead
Try / catch
try { models.add(m); } catch (IllegalStateException e) { requestModelBuild(); } Prevention
- Treat the controller model list as write-once inside buildModels().
- Use requestModelBuild() for every post-build change.
- Keep app data in separate collections.
- Avoid storing references to controller.getModelList()/the built list.
When it happens
Trigger: Mutating the controller's model list after buildModels() completes — e.g. adding to the list in a click callback, holding a reference to the controller's model list and calling add/addAll later, or mutating models inside onException/interceptors after finalization.
Common situations: Retaining the model list from buildModels and appending items in an async callback; calling add() on a list captured from a controller instead of building new models in buildModels().
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- You cannot call `requestModelBuild` directly. Call…
- Must have stable ids when saving view holder state
- State cannot be restored once views have been bound. It…
- Tried to restore instance state, but onSaveInstanceState…
- numItemsToPrefetch must be greater than 0
AI-assisted analysis of airbnb/epoxy@e45bd3a61f (2026-09-13).
Data as JSON: /api/errors/f17b9d286f367f5b.
Report an issue: GitHub.
Appendix: source
Thrown at epoxy-adapter/src/main/java/com/airbnb/epoxy/ControllerModelList.java:15
package com.airbnb.epoxy;
/**
* This ArrayList subclass enforces that no changes are made to the list after {@link #freeze()} is
* called. This prevents model interceptors from storing the list and trying to change it later. We
* could copy the list before diffing, but that would waste memory to make the copy for every
* buildModels cycle, plus the interceptors could still try to modify the list and be confused about
* why it doesn't do anything.
*/
class ControllerModelList extends ModelList {
private static final ModelListObserver OBSERVER = new ModelListObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
throw new IllegalStateException(
"Models cannot be changed once they are added to the controller");
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
throw new IllegalStateException(
"Models cannot be changed once they are added to the controller");
}
};
ControllerModelList(int expectedModelCount) {
super(expectedModelCount);
pauseNotifications();
}
void freeze() {
setObserver(OBSERVER);
resumeNotifications();View on GitHub (pinned to e45bd3a61f)