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

  1. Rebuild the model inside buildModels with the new value instead of mutating it
  2. Use controller.requestModelBuild() to trigger a rebuild after data changes
  3. Perform the mutation inside an Interceptor callback, which is exempt from the check
  4. 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

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


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)