{"record":{"id":"3f87f14f726deb12","repo":"can1357/oh-my-pi","slug":"parse-error-unclosed-template-expression","errorCode":null,"errorMessage":"Parse error: unclosed template expression","messagePattern":"Parse error: unclosed template expression","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/utils/src/template.ts","lineNumber":224,"sourceCode":"}\n\nfunction findTagEnd(source: string, start: number, triple: boolean): number {\n\tconst close = triple ? \"}}}\" : \"}}\";\n\tlet quote = \"\";\n\tlet depth = 0;\n\tfor (let index = start; index <= source.length - close.length; index++) {\n\t\tconst char = source[index];\n\t\tif (quote) {\n\t\t\tif (char === \"\\\\\") index++;\n\t\t\telse if (char === quote) quote = \"\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === '\"' || char === \"'\") quote = char;\n\t\telse if (char === \"(\") depth++;\n\t\telse if (char === \")\") depth--;\n\t\telse if (depth === 0 && source.startsWith(close, index)) return index;\n\t}\n\tthrow new Error(\"Parse error: unclosed template expression\");\n}\n\nfunction parseTemplate(source: string): Node[] {\n\tconst root: Node[] = [];\n\tconst stack: { node: BlockNode; target: Node[]; inverted: boolean }[] = [];\n\tlet target = root;\n\tlet cursor = 0;\n\twhile (cursor < source.length) {\n\t\tconst open = source.indexOf(\"{{\", cursor);\n\t\tif (open < 0) {\n\t\t\tif (cursor < source.length) target.push({ kind: \"text\", value: source.slice(cursor) });\n\t\t\tbreak;\n\t\t}\n\t\tif (open > cursor) target.push({ kind: \"text\", value: source.slice(cursor, open) });\n\t\tif (source.startsWith(\"{{!--\", open)) {\n\t\t\tconst end = source.indexOf(\"--}}\", open + 6);\n\t\t\tif (end < 0) throw new Error(\"Parse error: unclosed comment\");\n\t\t\tcursor = end + 4;","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/template.ts#L206-L242","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (template.md)\nValue is ${config.value\n// after\nValue is ${config.value}","handlingStrategy":"validation","validationCode":"let depth = 0, inQuote = null;\nfor (let i = 0; i < template.length; i++) {\n  const c = template[i];\n  if (inQuote) { if (c === inQuote) inQuote = null; continue; }\n  if (c === '\"' || c === \"'\") inQuote = c;\n  else if (c === '{' && template[i-1] === '$') depth++;\n  else if (c === '}') depth--;\n  if (depth < 0) throw new Error(`stray } at ${i}`);\n}\nif (depth !== 0) throw new Error('template has unclosed ${...} expression');","typeGuard":null,"tryCatchPattern":"try {\n  nodes = parseTemplate(source);\n} catch (err) {\n  if (String(err.message).includes('unclosed template expression')) {\n    throw new Error(`template file ${path} is malformed: ${err.message}`);\n  } else throw err;\n}","preventionTips":["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."],"tags":["template","parsing","syntax-error"],"backgroundTag":"unclosed-template-expression","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}