can1357/oh-my-pi · error

Parse error: unclosed template expression

Error message

Parse error: unclosed template expression

What it means

Template parsing scans forward from an opening `${` to locate the matching closing brace, tracking quotes and parenthesis depth. If the end of the source is reached with the expression still open (depth never returns to 0 and `}` never found outside quotes/parens), the template is malformed and this parse error is thrown.

Source

Thrown at packages/utils/src/template.ts:224

}

function findTagEnd(source: string, start: number, triple: boolean): number {
	const close = triple ? "}}}" : "}}";
	let quote = "";
	let depth = 0;
	for (let index = start; index <= source.length - close.length; index++) {
		const char = source[index];
		if (quote) {
			if (char === "\\") index++;
			else if (char === quote) quote = "";
			continue;
		}
		if (char === '"' || char === "'") quote = char;
		else if (char === "(") depth++;
		else if (char === ")") depth--;
		else if (depth === 0 && source.startsWith(close, index)) return index;
	}
	throw new Error("Parse error: unclosed template expression");
}

function parseTemplate(source: string): Node[] {
	const root: Node[] = [];
	const stack: { node: BlockNode; target: Node[]; inverted: boolean }[] = [];
	let target = root;
	let cursor = 0;
	while (cursor < source.length) {
		const open = source.indexOf("{{", cursor);
		if (open < 0) {
			if (cursor < source.length) target.push({ kind: "text", value: source.slice(cursor) });
			break;
		}
		if (open > cursor) target.push({ kind: "text", value: source.slice(cursor, open) });
		if (source.startsWith("{{!--", open)) {
			const end = source.indexOf("--}}", open + 6);
			if (end < 0) throw new Error("Parse error: unclosed comment");
			cursor = end + 4;

View on GitHub (pinned to 9690622007)

Solutions

  1. Locate the unterminated `${` in the template source and add the missing closing `}`.
  2. Escape literal `${` sequences if the template syntax supports it, or rewrite to avoid the `${` text.
  3. Validate templates at load time in tests so truncation is caught before runtime.
  4. Check any preprocessing/truncation step that could cut the template mid-expression.

Example fix

// before (template.md)
Value is ${config.value
// after
Value is ${config.value}
Defensive patterns

Strategy: validation

Validate before calling

let depth = 0, inQuote = null;
for (let i = 0; i < template.length; i++) {
  const c = template[i];
  if (inQuote) { if (c === inQuote) inQuote = null; continue; }
  if (c === '"' || c === "'") inQuote = c;
  else if (c === '{' && template[i-1] === '$') depth++;
  else if (c === '}') depth--;
  if (depth < 0) throw new Error(`stray } at ${i}`);
}
if (depth !== 0) throw new Error('template has unclosed ${...} expression');

Try / catch

try {
  nodes = parseTemplate(source);
} catch (err) {
  if (String(err.message).includes('unclosed template expression')) {
    throw new Error(`template file ${path} is malformed: ${err.message}`);
  } else throw err;
}

Prevention

When it happens

Trigger: A template string contains `${` without a matching `}` — e.g. truncated template content, hand-edited templates deleting the closing brace, or a literal `${` that should have been escaped appearing in user/template content.

Common situations: Editing .md prompt templates by hand and dropping a brace; pasting shell or code snippets containing `${...}` into templates without escaping; string truncation from earlier processing steps.

Understand the failure class

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/3f87f14f726deb12. Report an issue: GitHub.