apache/iceberg · error · IllegalStateException

Found already bound predicate:

Error message

Found already bound predicate: 

What it means

Binder's predicate method is only supposed to receive UnboundPredicate instances, which it binds to a struct's schema. If an already-bound BoundPredicate is passed, it means the expression is being bound twice, which is a bug in the caller's logic, so Binder throws this IllegalStateException.

Source

Thrown at api/src/main/java/org/apache/iceberg/expressions/Binder.java:154

    @Override
    public Expression not(Expression result) {
      return Expressions.not(result);
    }

    @Override
    public Expression and(Expression leftResult, Expression rightResult) {
      return Expressions.and(leftResult, rightResult);
    }

    @Override
    public Expression or(Expression leftResult, Expression rightResult) {
      return Expressions.or(leftResult, rightResult);
    }

    @Override
    public <T> Expression predicate(BoundPredicate<T> pred) {
      throw new IllegalStateException("Found already bound predicate: " + pred);
    }

    @Override
    public <T> Expression predicate(UnboundPredicate<T> pred) {
      return pred.bind(struct, caseSensitive);
    }

    @Override
    public <T> Expression aggregate(UnboundAggregate<T> agg) {
      return agg.bind(struct, caseSensitive);
    }

    @Override
    public <T, C> Expression aggregate(BoundAggregate<T, C> agg) {
      throw new IllegalStateException("Found already bound aggregate: " + agg);
    }
  }

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Bind each expression exactly once and reuse the BoundExpression result instead of re-binding
  2. Track binding state (keep unbound and bound expressions in separate variables)
  3. If unsure, use ExpressionVisitors.isBoundPredicate / expression().isBound() checks before calling bind
  4. If the expression may already be bound, short-circuit: only call bind when !expr.isBound()

Example fix

// before
Expression bound = Binder.bind(schema, expr, caseSensitive);
Expression rebound = Binder.bind(schema, bound, caseSensitive); // throws
// after
Expression bound = Binder.bind(schema, expr, caseSensitive);
if (!bound.isBound()) {
  bound = Binder.bind(schema, bound, caseSensitive);
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (expr.isBound()) { throw new IllegalArgumentException("expression already bound; pass an unbound expression"); }

Type guard

boolean needsBinding = expr instanceof UnboundPredicate;

Try / catch

try { bound = Binder.bind(schema, expr, caseSensitive); } catch (IllegalStateException e) { if (e.getMessage().startsWith("Found already bound")) { bound = expr; } else { throw e; } }

Prevention

When it happens

Trigger: Calling ExpressionBinder.bind() (or Expressions.bind) on an expression that was already bound, e.g. re-binding the result of a previous bind, or passing a BoundPredicate produced by a scan/residual evaluation into bind() a second time.

Common situations: Caching Expression objects and forgetting whether they were bound; applying residual-expression logic that feeds bound expressions back through Binder; framework code that binds user filters once at planning and again at execution.

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 apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/63983df15b43a8be. Report an issue: GitHub.