can1357/oh-my-pi · error

Parse error: unexpected else

Error message

Parse error: unexpected else

What it means

During parseTemplate(), an `{{else}}` (or `{{else helper}}`) token is only valid while a block (`{{#if}}`, `{{#each}}`, `{{^}}`...) is open. If the block stack is empty when `else` is encountered, there is no block to attach the inverse section to, so the parser throws "Parse error: unexpected else".

Source

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

			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;
			continue;
		}
		const triple = source.startsWith("{{{", open);
		const contentStart = open + (triple ? 3 : 2);
		const end = findTagEnd(source, contentStart, triple);
		const raw = source.slice(contentStart, end).trim();
		cursor = end + (triple ? 3 : 2);
		if (!raw || raw.startsWith("!")) continue;
		if (raw === "else" || raw.startsWith("else ")) {
			const current = stack[stack.length - 1];
			if (!current) throw new Error("Parse error: unexpected else");
			target = current.node.inverse;
			if (raw.length > 4) {
				const parentTarget = target;
				const nested: BlockNode = {
					kind: "block",
					expression: parseCall(raw.slice(5)),
					body: [],
					inverse: [],
					inverted: false,
				};
				target.push(nested);
				target = nested.body;
				stack.push({ node: nested, target: parentTarget, inverted: false });
			}
			continue;
		}
		if (raw.startsWith("#") || raw.startsWith("^")) {
			const inverted = raw.startsWith("^");

View on GitHub (pinned to 9690622007)

Solutions

  1. Find the `{{else}}` and either delete it or restore the opening block (`{{#if ...}}` / `{{#each ...}}`) it belongs to.
  2. Check for an extra `{{/if}}` or `{{/each}}` earlier in the template that closes the block before the else.
  3. Count block opens vs closes (`#`/`^` vs `/`) to confirm the else sits inside exactly one open block.

Example fix

// before
{{#if user}}Hi{{/if}}
{{else}}Anonymous
// after
{{#if user}}Hi{{else}}Anonymous{{/if}}
Defensive patterns

Strategy: validation

Validate before calling

function validateElsePlacement(src) {
  const tokens = src.match(/\{\{[#/^]?[^}]*\}\}/g) ?? [];
  let depth = 0;
  for (const t of tokens) {
    const raw = t.slice(2, -2).trim();
    if (raw === 'else' || raw.startsWith('else ')) {
      if (depth === 0) return { ok: false, at: t };
    } else if (raw.startsWith('#') || raw.startsWith('^')) depth++;
    else if (raw.startsWith('/')) depth--;
  }
  return { ok: true };
}
const check = validateElsePlacement(src); if (!check.ok) throw new Error(`{{else}} outside block at ${check.at}`);

Try / catch

try {
  render(src, data);
} catch (err) {
  if (err instanceof Error && err.message === 'Parse error: unexpected else') {
    throw new Error('Template has {{else}} outside any open block — fix template source');
  }
  throw err;
}

Prevention

When it happens

Trigger: Parsing a template that contains `{{else}}` outside any open block — e.g. after the enclosing `{{#if ...}}...{{/if}}` was already closed, or a stray `{{else}}` at top level.

Common situations: Manually editing templates and deleting the opening `{{#if}}` while keeping the `{{else}}`; renaming a block and accidentally closing it early; generated templates where conditional logic was removed but the else branch remained.

Understand the failure class

Related errors


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