can1357/oh-my-pi · error

Parse error: mismatched ${raw}

Error message

Parse error: mismatched ${raw}

What it means

When parseTemplate() sees a closing tag `{{/name}}`, it pops the open-block stack and requires that the top block has the same name. If the stack is empty (a close with no open block) or the names differ, it throws `Parse error: mismatched /name`. This catches mis-nested or mis-named block closers at parse time.

Source

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

		}
		if (raw.startsWith("#") || raw.startsWith("^")) {
			const inverted = raw.startsWith("^");
			const node: BlockNode = {
				kind: "block",
				expression: parseCall(raw.slice(1)),
				body: [],
				inverse: [],
				inverted,
			};
			target.push(node);
			stack.push({ node, target, inverted });
			target = node.body;
			continue;
		}
		if (raw.startsWith("/")) {
			const current = stack.pop();
			if (!current || current.node.expression.name !== raw.slice(1).trim())
				throw new Error(`Parse error: mismatched ${raw}`);
			if (current.inverted) [current.node.body, current.node.inverse] = [current.node.inverse, current.node.body];
			target = current.target;
			continue;
		}
		if (raw.startsWith(">")) target.push({ kind: "partial", expression: parseCall(raw.slice(1)) });
		else
			target.push({
				kind: "output",
				expression: parseCall(raw.startsWith("&") ? raw.slice(1) : raw),
				escaped: !triple && !raw.startsWith("&"),
			});
	}
	if (stack.length) throw new Error(`Parse error: unclosed block ${stack[stack.length - 1].node.expression.name}`);
	return root;
}

function pathParts(path: string): string[] {
	const parts: string[] = [];

View on GitHub (pinned to 9690622007)

Solutions

  1. Make each closing tag's name match its opening block (`{{#if x}}...{{/if}}`, `{{#each list}}...{{/each}}`).
  2. Verify nesting order — inner blocks must be closed before outer blocks.
  3. Remove any closer whose block opener was deleted.

Example fix

// before
{{#each items}}
  {{#if active}}{{name}}
{{/each}}
{{/if}}
// after
{{#each items}}
  {{#if active}}{{name}}{{/if}}
{{/each}}
Defensive patterns

Strategy: validation

Validate before calling

function validateBlockNames(src) {
  const stack = [];
  for (const t of src.match(/\{\{[#/^][^}]*\}\}/g) ?? []) {
    const raw = t.slice(2, -2).trim();
    if (raw.startsWith('#') || raw.startsWith('^')) stack.push(raw.slice(1).split(/[\s(]/)[0]);
    else if (raw.startsWith('/')) {
      const name = raw.slice(1).trim();
      if (stack.pop() !== name) return { ok: false, tag: raw };
    }
  }
  return { ok: stack.length === 0, tag: stack[0] };
}

Try / catch

try {
  render(src, data);
} catch (err) {
  const m = /^Parse error: mismatched (.+)$/.exec(err?.message ?? '');
  if (m) throw new Error(`Block closer ${m[1]} does not match the open block — fix nesting/naming`);
  throw err;
}

Prevention

When it happens

Trigger: Rendering a template where `{{/each}}` closes an `{{#if}}` block, `{{/if}}` appears with no open block, or closers are nested in the wrong order (e.g. `{{#each}}{{#if}}{{/each}}{{/if}}`).

Common situations: Hand-editing templates and swapping sections around; copy-pasting a block but forgetting to change its closer name; IDE auto-inserting `{{/if}}` after an `{{#each}}` edit.

Understand the failure class

Related errors


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