{"record":{"id":"a1654d27b486340d","repo":"can1357/oh-my-pi","slug":"missing-helper-call-name","errorCode":null,"errorMessage":"Missing helper: \"${call.name}\"","messagePattern":"Missing helper: \"(.+?)\"","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/utils/src/template.ts","lineNumber":432,"sourceCode":"}\n\nfunction evaluateCall(\n\tcall: CallExpression,\n\tframe: Frame,\n\tevaluation: Evaluation,\n\tforceHelper: boolean,\n\tbody: Node[] = [],\n\tinverse: Node[] = [],\n): unknown {\n\tconst helper = findHelper(call.name, evaluation);\n\tconst args = call.args.map(argument => evaluateExpression(argument, frame, evaluation));\n\tconst hash = evaluateHash(call.hash, frame, evaluation);\n\tif (helper)\n\t\treturn helper.call(frame.context, ...args, helperOptions(call.name, hash, frame, evaluation, body, inverse));\n\t// Handlebars built-in: `{{lookup obj key}}` → proto-safe `obj[key]`.\n\t// Resolved after user helpers so a registered `lookup` override wins.\n\tif (call.name === \"lookup\" && args.length >= 2) return property(args[0], String(args[1]));\n\tif (forceHelper || args.length) throw new Error(`Missing helper: \"${call.name}\"`);\n\tfor (const _key in hash) throw new Error(`Missing helper: \"${call.name}\"`);\n\treturn resolvePath(call.name, frame);\n}\n\nfunction evaluateBlock(node: BlockNode, frame: Frame, evaluation: Evaluation): string {\n\tconst { name } = node.expression;\n\tconst helper = findHelper(name, evaluation);\n\tif (helper) return stringify(evaluateCall(node.expression, frame, evaluation, true, node.body, node.inverse), false);\n\tconst args = node.expression.args.map(argument => evaluateExpression(argument, frame, evaluation));\n\tconst value = args.length ? args[0] : resolvePath(name, frame);\n\tconst hash = evaluateHash(node.expression.hash, frame, evaluation);\n\tif (name === \"if\" || name === \"unless\") {\n\t\tconst truthy = isConditionalTruthy(value, hash.includeZero === true);\n\t\tconst branch = name === \"if\" ? truthy : !truthy;\n\t\treturn renderNodes(branch ? node.body : node.inverse, frame, evaluation);\n\t}\n\tif (name === \"each\") {\n\t\tif (Array.isArray(value)) {","sourceCodeStart":414,"sourceCodeEnd":450,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/template.ts#L414-L450","documentation":"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.","triggerScenarios":"Rendering `{{helperName arg}}` or a subexpression `{{concat a b}}` where `helperName` was never registered via the helpers/runtime options and is not `lookup`.","commonSituations":"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.","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}}`."],"exampleFix":"// before\nrender(src, data); // template uses {{upcase name}}\n// after\nrender(src, data, { helpers: { upcase: (s) => String(s).toUpperCase() } });","handlingStrategy":"validation","validationCode":"function assertHelpersRegistered(src, helpers = {}) {\n  const called = new Set();\n  for (const t of src.match(/\\{\\{[^}]*\\}\\}/g) ?? []) {\n    const raw = t.replace(/^[{]{2,3}|[}]{2,3}$/g, '').trim();\n    const m = /^([\\w.]+)\\s*\\(/.exec(raw); // parenthesized call or args imply helper\n    const name = (m?.[1] ?? (raw.includes(' ') ? raw.split(/\\s+/)[0] : null));\n    if (raw.includes('(') || raw.match(/[\\w.]+\\s+[^\\s=]/)) if (name) called.add(name);\n  }\n  const missing = [...called].filter(n => !(n in helpers) && n !== 'lookup');\n  if (missing.length) throw new Error(`Unregistered helpers used in template: ${missing.join(', ')}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  return render(src, data, { helpers });\n} catch (err) {\n  const m = /^Missing helper: \"(.+)\"$/.exec(err?.message ?? '');\n  if (m) throw new Error(`Helper \"${m[1]}\" must be registered in the helpers option`);\n  throw err;\n}","preventionTips":["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."],"tags":["template","handlebars","missing-helper"],"backgroundTag":"missing-helper-registration","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}