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
- Locate the unterminated `${` in the template source and add the missing closing `}`.
- Escape literal `${` sequences if the template syntax supports it, or rewrite to avoid the `${` text.
- Validate templates at load time in tests so truncation is caught before runtime.
- 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
- Add a template-syntax test that parses every bundled .md template at CI time.
- Escape literal ${ sequences when embedding shell/code samples in templates.
- Avoid string-truncation steps that could cut a template mid-expression.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Unexpected end of JSON input
- Unexpected token at position ${this.#i}
- Replacement text is not valid UTF-8: {err}
- V2 compaction stream parse failed: ${err instanceof Error ?
- ${source} response is missing credentials.
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/3f87f14f726deb12.
Report an issue: GitHub.