airbnb/epoxy · error · IllegalArgumentException

Controller cannot be null

Error message

Controller cannot be null

What it means

addWithDebugValidation is the internal hook generated models call when debug validation (validateEpoxyModelUsage) is enabled. It requires the owning EpoxyController; a null controller means the model's usage cannot be validated, so Epoxy throws IllegalArgumentException immediately.

Solutions

  1. Always add models inside a controller's buildModels method so a valid controller is provided
  2. Ensure the controller reference passed to the model is non-null before adding
  3. Disable validateEpoxyModelUsage only if you truly use models without a controller

Example fix

// before
model.addWithDebugValidation(null);
// after
@Override public void buildModels(List<EpoxyModel<?>> models) { model.addWithDebugValidation(this); }
Defensive patterns

Strategy: type-guard

Validate before calling

if (controller == null) { throw new IllegalStateException("Controller must be assigned before adding models"); }

Type guard

if (!(model instanceof GeneratedModel<?> g) || g.controller() == null) { skipAdd(); }

Try / catch

try { model.addTo(controller); } catch (IllegalArgumentException e) { Log.e(TAG, "Model added without controller", e); }

Prevention

When it happens

Trigger: A generated model's addWithDebugValidation(controller) is invoked with a null controller argument, typically when the model's controller reference was never set or was passed as null from generated code.

Common situations: Manually invoking internal API; generated model used outside a controller's buildModels scope while validation is enabled; a controller field not yet assigned when the model is added.

Related errors


AI-assisted analysis of airbnb/epoxy@e45bd3a61f (2026-09-13). Data as JSON: /api/errors/d50f42d4be1b8d0f. Report an issue: GitHub.

Appendix: source

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

   */
  public void addIf(@NonNull AddPredicate predicate, @NonNull EpoxyController controller) {
    addIf(predicate.addIf(), controller);
  }

  /**
   * @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

View on GitHub (pinned to e45bd3a61f)