can1357/oh-my-pi · error
Missing helper: "${call.name}"
Error message
Missing helper: "${call.name}" What it means
In evaluateCall(), when a mustache call resolves to neither a registered helper nor the built-in `lookup`, and it was invoked with arguments (or forceHelper is set), the engine throws `Missing helper: "<name>"`. Handlebars treats a parenthesized call as a helper invocation, so an unknown helper name is fatal rather than falling back to path resolution.
Source
Thrown at packages/utils/src/template.ts:432
}
function evaluateCall(
call: CallExpression,
frame: Frame,
evaluation: Evaluation,
forceHelper: boolean,
body: Node[] = [],
inverse: Node[] = [],
): unknown {
const helper = findHelper(call.name, evaluation);
const args = call.args.map(argument => evaluateExpression(argument, frame, evaluation));
const hash = evaluateHash(call.hash, frame, evaluation);
if (helper)
return helper.call(frame.context, ...args, helperOptions(call.name, hash, frame, evaluation, body, inverse));
// Handlebars built-in: `{{lookup obj key}}` → proto-safe `obj[key]`.
// Resolved after user helpers so a registered `lookup` override wins.
if (call.name === "lookup" && args.length >= 2) return property(args[0], String(args[1]));
if (forceHelper || args.length) throw new Error(`Missing helper: "${call.name}"`);
for (const _key in hash) throw new Error(`Missing helper: "${call.name}"`);
return resolvePath(call.name, frame);
}
function evaluateBlock(node: BlockNode, frame: Frame, evaluation: Evaluation): string {
const { name } = node.expression;
const helper = findHelper(name, evaluation);
if (helper) return stringify(evaluateCall(node.expression, frame, evaluation, true, node.body, node.inverse), false);
const args = node.expression.args.map(argument => evaluateExpression(argument, frame, evaluation));
const value = args.length ? args[0] : resolvePath(name, frame);
const hash = evaluateHash(node.expression.hash, frame, evaluation);
if (name === "if" || name === "unless") {
const truthy = isConditionalTruthy(value, hash.includeZero === true);
const branch = name === "if" ? truthy : !truthy;
return renderNodes(branch ? node.body : node.inverse, frame, evaluation);
}
if (name === "each") {
if (Array.isArray(value)) {View on GitHub (pinned to 9690622007)
Solutions
- Register the helper: pass it in the helpers map (or runtime.helpers) when invoking the template engine.
- Fix the typo / update the template to a helper that exists.
- If the name is really data (a path lookup), remove the arguments so it falls back to `resolvePath` — e.g. `{{name}}` instead of `{{name arg}}`.
Example fix
// before
render(src, data); // template uses {{upcase name}}
// after
render(src, data, { helpers: { upcase: (s) => String(s).toUpperCase() } }); Defensive patterns
Strategy: validation
Validate before calling
function assertHelpersRegistered(src, helpers = {}) {
const called = new Set();
for (const t of src.match(/\{\{[^}]*\}\}/g) ?? []) {
const raw = t.replace(/^[{]{2,3}|[}]{2,3}$/g, '').trim();
const m = /^([\w.]+)\s*\(/.exec(raw); // parenthesized call or args imply helper
const name = (m?.[1] ?? (raw.includes(' ') ? raw.split(/\s+/)[0] : null));
if (raw.includes('(') || raw.match(/[\w.]+\s+[^\s=]/)) if (name) called.add(name);
}
const missing = [...called].filter(n => !(n in helpers) && n !== 'lookup');
if (missing.length) throw new Error(`Unregistered helpers used in template: ${missing.join(', ')}`);
} Try / catch
try {
return render(src, data, { helpers });
} catch (err) {
const m = /^Missing helper: "(.+)"$/.exec(err?.message ?? '');
if (m) throw new Error(`Helper "${m[1]}" must be registered in the helpers option`);
throw err;
} Prevention
- Keep a single shared helpers registry and pass it to every render call.
- Add a test that renders each template with the production helper set.
- When removing a helper, grep all templates for its name first.
When it happens
Trigger: Rendering `{{helperName arg}}` or a subexpression `{{concat a b}}` where `helperName` was never registered via the helpers/runtime options and is not `lookup`.
Common situations: Porting Handlebars templates into this engine without porting the custom helper registrations; typo in a helper name; registering helpers on a different render instance than the one rendering; helper removed in a refactor while templates still reference it.
Related errors
- Parse error: unclosed comment
- Parse error: unexpected else
- Parse error: mismatched ${raw}
- Parse error: unclosed block ${stack[stack.length - 1].node.e
- The partial ${node.expression.name} could not be found
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/a1654d27b486340d.
Report an issue: GitHub.