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 typeView on GitHub (pinned to 1c8a60f858)
Solutions
- Before calling toInternal, check the payload: if null or isEmpty, skip conversion and treat it as 'no filters' rather than passing it through.
- Validate the persisted shape on load and migrate/clear malformed {} entries.
- 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
- Treat null/{} as 'no filter' upstream
- Sanitize persisted payloads on load
- Never pass uninitialized form state to the adapter
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
- AND group must contain at least one condition
- Invalid expression: unknown structure with keys [${expressio
- Invalid relative amount: ${amountStr}
- Unsupported time unit: ${unit}
- Invalid date format: {value}
AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12).
Data as JSON: /api/errors/464fa452e537b720.
Report an issue: GitHub.