airbnb/epoxy · error · IllegalEpoxyUsage

This model was already added to the controller at position

Error message

This model was already added to the controller at position {position}

What it means

Each model instance can be added to a controller only once, because ids must be unique for diffing. When debug validation is enabled and the model is detected a second time in the controller's building list, Epoxy throws IllegalEpoxyUsage naming the first position it was added.

Solutions

  1. Create a new model instance for each item (e.g. in a loop: new ItemModel_().id(item.getId()))
  2. Remove the duplicate add of the same instance
  3. Give reused models distinct ids and build them fresh each pass
  4. Keep model construction inside buildModels instead of caching shared instances

Example fix

// before
ItemModel_ item = new ItemModel_();
for (Data d : list) { item.id(d.id); item.add(); } // same instance twice
// after
for (Data d : list) { new ItemModel_().id(d.id).addTo(this); }
Defensive patterns

Strategy: validation

Validate before calling

if (buildingModels.contains(model)) throw new IllegalStateException("Model added twice: " + model);

Try / catch

try { buildModelsInternal(); } catch (IllegalEpoxyUsage e) { Log.e(TAG, "Duplicate model add", e); }

Prevention

When it happens

Trigger: Adding the same EpoxyModel instance twice in buildModels (e.g. appending it to the list twice, or reusing one instance variable for multiple list items).

Common situations: Reusing a single field model for each item of a list instead of creating a new instance per item; accidentally calling add() on the model twice; adding a model to two lists that both get built.

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/626f1103139a7b14. Report an issue: GitHub.

Appendix: source

Thrown at epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModel.java:418

  /**
   * @see #addIf(AddPredicate, EpoxyController)
   */
  public interface AddPredicate {
    boolean addIf();
  }

  /**
   * This is used internally by generated models to turn on validation checking when
   * "validateEpoxyModelUsage" is enabled and the model is used with an {@link EpoxyController}.
   */
  protected final void addWithDebugValidation(@NonNull EpoxyController controller) {
    if (controller == null) {
      throw new IllegalArgumentException("Controller cannot be null");
    }

    if (controller.isModelAddedMultipleTimes(this)) {
      throw new IllegalEpoxyUsage(
          "This model was already added to the controller at position "
              + controller.getFirstIndexOfModelInBuildingList(this));
    }

    if (firstControllerAddedTo == null) {
      firstControllerAddedTo = controller;

      // We save the current hashCode so we can compare it to the hashCode at later points in time
      // in order to validate that it doesn't change and enforce mutability.
      hashCodeWhenAdded = hashCode();

      // The one time it is valid to change the model is during an interceptor callback. To support
      // that we need to update the hashCode after interceptors have been run.
      // The model can be added to multiple controllers, but we only allow an interceptor change
      // the first time, since after that it will have been added to an adapter.
      controller.addAfterInterceptorCallback(new ModelInterceptorCallback() {
        @Override
        public void onInterceptorsStarted(EpoxyController controller) {

View on GitHub (pinned to e45bd3a61f)