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
- Bind each expression exactly once and reuse the BoundExpression result instead of re-binding
- Track binding state (keep unbound and bound expressions in separate variables)
- If unsure, use ExpressionVisitors.isBoundPredicate / expression().isBound() checks before calling bind
- 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
- Keep unbound and bound expressions in distinct variables/fields
- Check Expression.isBound() before calling bind
- Never feed residual/bound expressions back through Binder
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
- Found already bound aggregate:
- Unsupported aggregate type: %s
- Invalid aggregate:
- does not implement eval(StructLike)
- does not implement eval(DataFile)
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/63983df15b43a8be.
Report an issue: GitHub.