airbnb/epoxy · error · ImmutableModelException
Epoxy attribute fields on a model cannot be changed once…
Error message
Epoxy attribute fields on a model cannot be changed once the model is added to a controller. Check that these fields are not updated, or that the assigned objects are not mutated, outside of the buildModels method. The only exception is if the change is made inside an Interceptor callback. Consider using an interceptor if you need to change a model after it is added to the controller and before it is set on the adapter. If the model is already set on the adapter then you must call `requestModelBuild` instead to recreate all models.
What it means
When debug validation is enabled, Epoxy models are immutable once added to a controller: mutating attribute fields outside buildModels (or an Interceptor callback) would invalidate the hash snapshot used for diffing. onMutation detects such a change and throws ImmutableModelException with the model's position.
Solutions
- Rebuild the model inside buildModels with the new value instead of mutating it
- Use controller.requestModelBuild() to trigger a rebuild after data changes
- Perform the mutation inside an Interceptor callback, which is exempt from the check
- Keep all model configuration at construction time inside buildModels
Example fix
// before model.show(false); // throws if model already added // after requestModelBuild(); // in buildModels: new HeaderModel_().show(false)
Defensive patterns
Strategy: validation
Validate before calling
if (model != null && addedModels.contains(model) && !inBuildModels) throw new IllegalStateException("Cannot mutate added model"); Try / catch
try { mutateModel(); } catch (ImmutableModelException e) { requestModelBuild(); } Prevention
- Mutate only inside buildModels
- Use interceptors for pre-adapter changes
- Use requestModelBuild() for post-adapter changes
- Don't cache model instances across builds
When it happens
Trigger: Calling any mutating setter (layout(), reset(), show(), etc.) on a model after it was added to a controller while outside interceptors and with validation enabled.
Common situations: Updating a retained model in an onClick handler or after data refresh without rebuilding; mutating models inside interceptors incorrectly; enabling validateEpoxyModelUsage on a controller that mutates models after build.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- You must set an id on a model before adding it. Use the…
- Controller cannot be null
- This model was already added to the controller at position
- Models cannot be empty
- Must have stable ids when saving view holder state
AI-assisted analysis of airbnb/epoxy@e45bd3a61f (2026-09-13).
Data as JSON: /api/errors/f54f1df63a5b12f0.
Report an issue: GitHub.
Appendix: source
Thrown at epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModel.java:468
return firstControllerAddedTo != null;
}
/**
* This is used internally by generated models to do validation checking when
* "validateEpoxyModelUsage" is enabled and the model is used with an {@link EpoxyController}.
* This method validates that it is ok to change this model. It is only valid if the model hasn't
* yet been added, or the change is being done from an {@link EpoxyController.Interceptor}
* callback.
* <p>
* This is also used to stage the model for implicitly adding it, if it is an AutoModel and
* implicit adding is enabled.
*/
protected final void onMutation() {
// The model may be added to multiple controllers, in which case if it was already diffed
// and added to an adapter in one controller we don't want to even allow interceptors
// from changing the model in a different controller
if (isDebugValidationEnabled() && !currentlyInInterceptors) {
throw new ImmutableModelException(this,
getPosition(firstControllerAddedTo, this));
}
if (controllerToStageTo != null) {
controllerToStageTo.setStagedModel(this);
}
}
private static int getPosition(@NonNull EpoxyController controller,
@NonNull EpoxyModel<?> model) {
// If the model was added to multiple controllers, or was removed from the controller and then
// modified, this won't be correct. But those should be very rare cases that we don't need to
// worry about
if (controller.isBuildingModels()) {
return controller.getFirstIndexOfModelInBuildingList(model);
}
return controller.getAdapter().getModelPosition(model);View on GitHub (pinned to e45bd3a61f)