makeplane/plane · error · Error

Invalid expression: empty or null data

Error message

Invalid expression: empty or null data

What it means

Thrown by the work-item filter adapter's private _convertExpressionToInternal when the incoming expression is null, undefined, or an empty object. This method recursively converts the external persisted filter shape into the internal TFilterExpression tree; an empty/null root has no condition or logical group to translate and is treated as malformed input rather than 'no filter'.

Source

Thrown at packages/shared-state/src/store/work-item-filters/adapter.ts:51

    try {
      return this._convertExpressionToInternal(externalFilter);
    } catch (error) {
      console.error("Failed to convert external filter to internal:", error);
      return null;
    }
  }

  /**
   * Recursively converts external expression data to internal filter tree
   * @param expression - The external expression data
   * @returns Internal filter expression
   */
  private _convertExpressionToInternal(
    expression: TWorkItemFilterExpressionData
  ): TFilterExpression<TWorkItemFilterProperty> {
    if (!expression || isEmpty(expression)) {
      throw new Error("Invalid expression: empty or null data");
    }

    // Check if it's a simple condition (has field property)
    if (this._isWorkItemFilterConditionData(expression)) {
      const conditionResult = this._extractWorkItemFilterConditionData(expression);
      if (!conditionResult) {
        throw new Error("Failed to extract condition data");
      }

      const [property, operator, value] = conditionResult;
      return createConditionNode({
        property,
        operator,
        value,
      });
    }

    // It's a logical group - check which type

View on GitHub (pinned to 1c8a60f858)

Solutions

  1. Before calling toInternal, check the payload: if null or isEmpty, skip conversion and treat it as 'no filters' rather than passing it through.
  2. Validate the persisted shape on load and migrate/clear malformed {} entries.
  3. If you intentionally want 'no filter', return early with an empty filter tree instead of invoking the adapter.

Example fix

// before
const tree = adapter.toInternal(expressionFromStore);

// after
import { isEmpty } from 'lodash';
const tree = !expressionFromStore || isEmpty(expressionFromStore)
  ? createEmptyFilter()
  : adapter.toInternal(expressionFromStore);
Defensive patterns

Strategy: validation

Validate before calling

import { isEmpty } from 'lodash';
function hasContent(expr: unknown): boolean {
  return expr != null && typeof expr === 'object' && !isEmpty(expr);
}
if (hasContent(expression)) adapter.toInternal(expression);

Type guard

function isNonEmptyExpression(e: unknown): e is Record<string, unknown> {
  return typeof e === 'object' && e !== null && Object.keys(e as object).length > 0;
}

Try / catch

try {
  tree = adapter.toInternal(expr);
} catch (e) {
  if (e instanceof Error && e.message === 'Invalid expression: empty or null data') {
    tree = createEmptyFilter();
  } else throw e;
}

Prevention

When it happens

Trigger: toInternal({}) or toInternal(null); a persisted filter whose serialized payload lost its keys during migration/JSON round-trip; calling the adapter with a default-empty state object from a form that was never filled.

Common situations: Loading a work-item view whose filter payload was stored as {} after an export/import; passing a partially-initialized filter state from a MobX store before the user picks anything.

Related errors


AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12). Data as JSON: /api/errors/464fa452e537b720. Report an issue: GitHub.