{"record":{"id":"e2ec9b6e107806cc","repo":"can1357/oh-my-pi","slug":"parse-error-unclosed-block-stack-stack-length","errorCode":null,"errorMessage":"Parse error: unclosed block ${stack[stack.length - 1].node.expression.name}","messagePattern":"Parse error: unclosed block (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/utils/src/template.ts","lineNumber":300,"sourceCode":"\t\t\tcontinue;\n\t\t}\n\t\tif (raw.startsWith(\"/\")) {\n\t\t\tconst current = stack.pop();\n\t\t\tif (!current || current.node.expression.name !== raw.slice(1).trim())\n\t\t\t\tthrow new Error(`Parse error: mismatched ${raw}`);\n\t\t\tif (current.inverted) [current.node.body, current.node.inverse] = [current.node.inverse, current.node.body];\n\t\t\ttarget = current.target;\n\t\t\tcontinue;\n\t\t}\n\t\tif (raw.startsWith(\">\")) target.push({ kind: \"partial\", expression: parseCall(raw.slice(1)) });\n\t\telse\n\t\t\ttarget.push({\n\t\t\t\tkind: \"output\",\n\t\t\t\texpression: parseCall(raw.startsWith(\"&\") ? raw.slice(1) : raw),\n\t\t\t\tescaped: !triple && !raw.startsWith(\"&\"),\n\t\t\t});\n\t}\n\tif (stack.length) throw new Error(`Parse error: unclosed block ${stack[stack.length - 1].node.expression.name}`);\n\treturn root;\n}\n\nfunction pathParts(path: string): string[] {\n\tconst parts: string[] = [];\n\tlet start = 0;\n\tlet bracketStart = -1;\n\tfor (let index = 0; index <= path.length; index++) {\n\t\tconst char = path[index];\n\t\tif (char === \"[\" && bracketStart < 0) {\n\t\t\tif (index > start) parts.push(path.slice(start, index).replace(/[./]+$/, \"\"));\n\t\t\tbracketStart = index + 1;\n\t\t} else if (char === \"]\" && bracketStart >= 0) {\n\t\t\tparts.push(path.slice(bracketStart, index));\n\t\t\tstart = index + 1;\n\t\t\tbracketStart = -1;\n\t\t} else if ((char === \".\" || char === \"/\" || char === undefined) && bracketStart < 0) {\n\t\t\tif (index > start) parts.push(path.slice(start, index));","sourceCodeStart":282,"sourceCodeEnd":318,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/template.ts#L282-L318","documentation":"After the main parse loop finishes, parseTemplate() checks that every opened block (`{{#x}}`/`{{^x}}`/`{{else x}}`) was closed. If any block remains on the stack, it throws `Parse error: unclosed block <name>` reporting the innermost unclosed helper name. The template cannot be compiled until all blocks are balanced.","triggerScenarios":"Rendering a template containing `{{#if ...}}`, `{{#each ...}}`, or `{{^...}}` without a matching `{{/...}}` closer before end of input.","commonSituations":"Truncated template files; appending new block content and forgetting the closer; dynamically generated templates where a conditional closer was omitted by code.","solutions":["Add the missing `{{/<name>}}` closer for the block named in the error.","Check the end of the template/file for truncation if the block was supposed to be closed.","Audit generated templates so every block-opening emission is paired with a closer emission."],"exampleFix":"// before\n{{#each items}}\n  {{name}}\n// after\n{{#each items}}\n  {{name}}\n{{/each}}","handlingStrategy":"validation","validationCode":"function allBlocksClosed(src) {\n  let depth = 0;\n  for (const t of src.match(/\\{\\{[#/^][^}]*\\}\\}/g) ?? []) {\n    const raw = t.slice(2, -2).trim();\n    if (raw.startsWith('#') || raw.startsWith('^')) depth++;\n    else if (raw.startsWith('/')) depth--;\n    if (depth < 0) return false;\n  }\n  return depth === 0;\n}\nif (!allBlocksClosed(src)) throw new Error('Template has unclosed {{#block}}');","typeGuard":null,"tryCatchPattern":"try {\n  render(src, data);\n} catch (err) {\n  const m = /^Parse error: unclosed block (.+)$/.exec(err?.message ?? '');\n  if (m) throw new Error(`Template ends with open block {{#${m[1]}}} — add {{/${m[1]}}}`);\n  throw err;\n}","preventionTips":["Write the closing tag immediately after the opening one and fill in the body between.","Validate template files parse in CI before they ship.","Watch for truncation when generating templates from code or copying between files."],"tags":["template","parse-error","handlebars"],"backgroundTag":"unclosed-block-tag","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}