airbnb/epoxy · error · IllegalEpoxyUsage

Can only call this when inside the `buildModels` method

Error message

Can only call this when inside the `buildModels` method

What it means

assertIsBuildingModels() enforces that certain controller APIs (adding models, querying modelsBeingBuilt, counting built models, registering interceptor callbacks) are only used during the buildModels() pass. Using them outside throws IllegalEpoxyUsage because the building list is empty or otherwise invalid outside a build.

Solutions

  1. Move model.addTo(controller) calls into buildModels()
  2. Wrap state queries so they only run during building (or guard with isBuildingModels())
  3. Move addAfterInterceptorCallback registration to the start of a buildModels pass

Example fix

// before
void onClick() {
  new HeaderModel_().id(1).addTo(controller); // crash
}
// after
@Override
protected void buildModels() {
  new HeaderModel_().id(1).addTo(this);
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (controller.isBuildingModels()) { model.addTo(controller); }

Type guard

boolean canAddModels(EpoxyController c) { return c.isBuildingModels(); }

Try / catch

try { model.addTo(controller); } catch (IllegalEpoxyUsage e) { /* move call into buildModels */ }

Prevention

When it happens

Trigger: Calling getFirstIndexOfModelInBuildingList, isModelAddedMultipleTimes, addAfterInterceptorCallback, getModelCountBuiltSoFar, or addInternal (e.g. model.addTo(controller)) outside of buildModels().

Common situations: Calling model.addTo(controller) from outside the controller's buildModels (e.g. in an adapter click handler or activity code); querying building-list state in a callback after the build finished.

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/6101d1654ce04dd5. Report an issue: GitHub.

Appendix: source

Thrown at epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java:452

    interceptors.remove(interceptor);
  }

  /**
   * Get the number of models added so far during the {@link #buildModels()} phase. It is only valid
   * to call this from within that method.
   * <p>
   * This is different from the number of models currently on the adapter, since models on the
   * adapter are not updated until after models are finished being built. To access current adapter
   * count call {@link #getAdapter()} and {@link EpoxyControllerAdapter#getItemCount()}
   */
  protected int getModelCountBuiltSoFar() {
    assertIsBuildingModels();
    return modelsBeingBuilt.size();
  }

  private void assertIsBuildingModels() {
    if (!isBuildingModels()) {
      throw new IllegalEpoxyUsage("Can only call this when inside the `buildModels` method");
    }
  }

  private void assertNotBuildingModels() {
    if (isBuildingModels()) {
      throw new IllegalEpoxyUsage("Cannot call this from inside `buildModels`");
    }
  }

  /**
   * Add the model to this controller. Can only be called from inside {@link
   * EpoxyController#buildModels()}.
   */
  public void add(@NonNull EpoxyModel<?> model) {
    model.addTo(this);
  }

  /**

View on GitHub (pinned to e45bd3a61f)