n8n-io/n8n · error · SyntaxError

Not a expression statement

Error message

Not a expression statement

What it means

SyntaxError('Not a expression statement') is thrown in the multi-chunk branch of ExpressionBuilder when jsVariablePolyfill returns no statement or a statement whose type is not 'ExpressionStatement'. n8n expressions in a templated string must each be a single expression; a statement (declaration, if, for, return) is not allowed.

Source

Thrown at packages/@n8n/tournament/src/ExpressionBuilder.ts:223

		chunks.length > 2 ||
		chunks[0].text !== '' ||
		// This is a blank expression. It should just return an empty string
		(chunks[0].text === '' && chunks.length === 1)
	) {
		let parts: ExpressionKind[] = [];
		for (const chunk of chunks) {
			// This is just a text chunks, push it up as a literal.
			if (chunk.type === 'text') {
				parts.push(b.literal(chunk.text));
				// This is a code chunk so do some magic
			} else {
				const fixed = fixStringNewLines(chunk.parsed);
				for (const hook of hooks.before) {
					hook(fixed, dataNode);
				}
				const parsed = jsVariablePolyfill(fixed, dataNode)?.[0];
				if (!parsed || parsed.type !== 'ExpressionStatement') {
					throw new SyntaxError('Not a expression statement');
				}

				for (const hook of hooks.after) {
					hook(parsed, dataNode);
				}

				const functionBody = buildFunctionBody(parsed.expression);

				if (shouldWrapInTry(parsed)) {
					// Wraps the body of our expression function in a try/catch
					// to match tmpl
					functionBody.body = [
						wrapInErrorHandler(functionBody.body[0]),
						// This is for tmpl compat. It puts a ; after the try/catch
						// creating an empty statement. emptyStatement is just printed
						// to nothing so we use an expression statement with a blank
						// identifier.
						b.expressionStatement(b.identifier('')),

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Rewrite the chunk as a single expression: use an IIFE '{{ (() => { let x = 1; return x; })() }}' if local variables are truly needed, or inline the logic.
  2. Move complex logic out of the expression and into a Code node / Set node, then reference the result from the expression.
  3. Remove statement keywords (let/const/return/if/for) from inside {{ }}.
  4. If parsing fails entirely, check for unbalanced braces or stray characters inside the chunk.

Example fix

// before
{{ let x = $json.value + 1; x * 2 }}
// after
{{ ($json.value + 1) * 2 }}
Defensive patterns

Strategy: validation

Validate before calling

// Reject statement keywords inside a multi-chunk expression before evaluating.
const stmtRe = /^\\s*(let|const|var|if|for|while|return|function|class|do|switch|throw)\\b/;
if (stmtRe.test(codeChunk.trim())) {
  throw new Error('expression chunks must be a single expression, not a statement');
}

Type guard

import { namedTypes } from 'ast-types';
function isExpressionStatement(
  node: unknown,
): node is namedTypes.ExpressionStatement {
  return !!node && (node as any).type === 'ExpressionStatement';
}

Try / catch

try {
  const compiled = buildExpression(chunks, hooks);
} catch (e) {
  if (e instanceof SyntaxError && /Not a expression statement/.test(e.message)) {
    throw new UserError('Each {{ }} must contain a single expression; use a Code node for statements.');
  }
  throw e;
}

Prevention

When it happens

Trigger: An n8n expression code chunk (inside {{ }}) contains something that is not a single expression: e.g. '{{ let x = 1 }}', '{{ if (true) {1} }}', '{{ return 5 }}', '{{ const y = 2 }}', or a chunk that fails to parse entirely so parsed is undefined.

Common situations: A user writes JS control-flow or variable declarations inside an n8n expression expecting it to behave like a script; a pasted code snippet includes statements; a migration from another templating engine brought in non-expression syntax.

Related errors


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