n8n-io/n8n · error · ExpressionExtensionError

renameKeys(): expected an even amount of args: from1, to1 [,

Error message

renameKeys(): expected an even amount of args: from1, to1 [, from2, to2, ...]. e.g. .renameKeys("name", "title")

What it means

Thrown as ExpressionExtensionError by the .renameKeys() array extension when extraArgs is empty or has an odd length. renameKeys takes key pairs (from1,to1,from2,to2,...) and renames matching keys on each object in the array; an odd count means one 'from' has no matching 'to', so the call is ambiguous and rejected.

Source

Thrown at packages/@n8n/expression-runtime/src/extensions/array-extensions.ts:191

		return o;
	}, {});
}

function chunk(value: unknown[], extraArgs: number[]) {
	const [chunkSize] = extraArgs;
	if (typeof chunkSize !== 'number' || chunkSize === 0) {
		throw new ExpressionExtensionError('chunk(): expected non-zero numeric arg, e.g. .chunk(5)');
	}
	const chunks: unknown[][] = [];
	for (let i = 0; i < value.length; i += chunkSize) {
		chunks.push(value.slice(i, i + chunkSize));
	}
	return chunks;
}

function renameKeys(value: unknown[], extraArgs: string[]): unknown[] {
	if (extraArgs.length === 0 || extraArgs.length % 2 !== 0) {
		throw new ExpressionExtensionError(
			'renameKeys(): expected an even amount of args: from1, to1 [, from2, to2, ...]. e.g. .renameKeys("name", "title")',
		);
	}
	return value.map((v) => {
		if (typeof v !== 'object' || v === null) {
			return v;
		}
		// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
		const newObj = { ...(v as any) };
		const chunkedArgs = chunk(extraArgs, [2]) as string[][];
		chunkedArgs.forEach(([from, to]) => {
			if (from in newObj) {
				// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
				newObj[to] = newObj[from];
				// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
				delete newObj[from];
			}
		});

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Pass arguments in pairs: .renameKeys('name','title').
  2. For multiple renames, keep the count even: .renameKeys('name','title','age','years').
  3. If building args dynamically, assert args.length % 2 === 0 before calling.
  4. Check that template-expanded argument lists didn't drop a value.

Example fix

// before — odd number of args
{{ $json.rows.renameKeys('name', 'title', 'age') }}
// after — even number of args (pairs)
{{ $json.rows.renameKeys('name', 'title', 'age', 'years') }}
Defensive patterns

Strategy: validation

Validate before calling

function renameKeysSafe(arr, ...pairs) {
  if (pairs.length === 0 || pairs.length % 2 !== 0) {
    throw new TypeError('renameKeys requires an even number of args: from1, to1, ...');
  }
  // ...proceed
}

Type guard

function hasEvenNonEmptyArgs(args) { return Array.isArray(args) && args.length > 0 && args.length % 2 === 0; }

Try / catch

try {
  result = arr.renameKeys('name', 'title');
} catch (e) {
  if (e.name === 'ExpressionExtensionError' && /renameKeys/i.test(e.message)) {
    // supply args in pairs: from1, to1 [, from2, to2, ...]
  }
}

Prevention

When it happens

Trigger: Calling .renameKeys() with zero arguments, or an odd number of arguments. Examples: [...].renameKeys() (empty), [...].renameKeys('name') (one arg, odd), [...].renameKeys('a','b','c') (three args, odd).

Common situations: Forgetting the second half of a pair. Building the args list dynamically and dropping one element. Copy-paste error leaving a dangling 'from' without a 'to'.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/21ce25ed83051998. Report an issue: GitHub.